@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/lib/sse-endpoint.js
CHANGED
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Milky SSE client endpoint — GET text/event-stream on /event.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
5
5
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
6
6
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createMilkyActionCaller, readMilkyResponse, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
|
|
8
8
|
import { buildSendAction, buildSseConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
|
|
9
9
|
import { openSseStream } from './sse-client.js';
|
|
10
|
-
export class MilkySseEndpoint extends
|
|
11
|
-
|
|
10
|
+
export class MilkySseEndpoint extends MilkyClient {
|
|
11
|
+
[endpointSpec];
|
|
12
|
+
#open = false;
|
|
12
13
|
#logger;
|
|
13
14
|
#options;
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
control = createRecallEndpointControl((id) => this
|
|
15
|
+
#management;
|
|
16
|
+
#call;
|
|
17
|
+
#control = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
|
|
17
18
|
#lifecycle;
|
|
18
19
|
#stream;
|
|
19
20
|
constructor(options) {
|
|
20
|
-
|
|
21
|
+
const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
|
|
22
|
+
super(milkyClientOptions({ config: options.config, call }));
|
|
23
|
+
this.#call = call;
|
|
21
24
|
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
22
25
|
this.#options = options;
|
|
23
|
-
this.#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
this.#management = createMilkyEndpointManagement({
|
|
27
|
+
callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
|
|
28
|
+
});
|
|
29
|
+
const readTransportState = () => this.#lifecycle.state;
|
|
30
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
31
|
+
name: options.config.id,
|
|
32
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
33
|
+
close: () => { this.#open = false; }, stop: () => this.#stop(),
|
|
34
|
+
send: request => this.#send(request),
|
|
35
|
+
management: this.#management, control: this.#control,
|
|
36
|
+
get transportState() { return readTransportState(); },
|
|
37
|
+
});
|
|
38
|
+
forwardMilkyClientEvents(this, (name, payload) => {
|
|
39
|
+
void this[endpointSpec].events.run(() => {
|
|
40
|
+
if (!this.#open && !this.#lifecycle.started)
|
|
41
|
+
return;
|
|
42
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
43
|
+
if (name === 'event')
|
|
44
|
+
this.#admitRaw(payload);
|
|
45
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
27
46
|
});
|
|
28
|
-
this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
|
|
29
|
-
if (name === 'event')
|
|
30
|
-
this.#admitRaw(payload);
|
|
31
|
-
}, (_name, error) => this.#warnPlatformEvent(error));
|
|
32
47
|
this.#lifecycle = createEndpointLifecycle({
|
|
33
48
|
name: options.config.id,
|
|
34
49
|
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
@@ -40,7 +55,7 @@ export class MilkySseEndpoint extends ClientEndpoint {
|
|
|
40
55
|
},
|
|
41
56
|
});
|
|
42
57
|
}
|
|
43
|
-
async start() {
|
|
58
|
+
async #start() {
|
|
44
59
|
if (this.#lifecycle.started)
|
|
45
60
|
return;
|
|
46
61
|
try {
|
|
@@ -51,16 +66,16 @@ export class MilkySseEndpoint extends ClientEndpoint {
|
|
|
51
66
|
throw err;
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
|
-
async stop() {
|
|
55
|
-
this
|
|
69
|
+
async #stop() {
|
|
70
|
+
this.#open = false;
|
|
56
71
|
await this.#lifecycle.stop();
|
|
57
72
|
this.#stream?.close();
|
|
58
73
|
this.#stream = undefined;
|
|
59
74
|
}
|
|
60
|
-
async send({ conversation, payload }) {
|
|
75
|
+
async #send({ conversation, payload, signal }) {
|
|
61
76
|
const message = formatOutboundSegments(payload);
|
|
62
77
|
const { action, params } = buildSendAction(conversation, message);
|
|
63
|
-
const data = await
|
|
78
|
+
const data = readMilkyResponse(await this.#call({ action, params, signal }), action);
|
|
64
79
|
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
65
80
|
this.#logger.debug(formatCompact({
|
|
66
81
|
op: 'milky_send',
|
|
@@ -71,24 +86,26 @@ export class MilkySseEndpoint extends ClientEndpoint {
|
|
|
71
86
|
}));
|
|
72
87
|
return messageId;
|
|
73
88
|
}
|
|
74
|
-
async recallMessage(id) {
|
|
89
|
+
async #recallMessage({ id, signal }) {
|
|
75
90
|
const parsed = parseMilkyMessageId(id);
|
|
76
91
|
if (!parsed)
|
|
77
92
|
throw new Error(`Invalid message id: ${id}`);
|
|
78
93
|
if (parsed.message_scene === 'group') {
|
|
79
|
-
await
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
|
|
95
|
+
group_id: parsed.peer_id,
|
|
96
|
+
message_seq: parsed.message_seq,
|
|
97
|
+
} }), 'recall_group_message');
|
|
83
98
|
}
|
|
84
99
|
else {
|
|
85
|
-
await
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
|
|
101
|
+
user_id: parsed.peer_id,
|
|
102
|
+
message_seq: parsed.message_seq,
|
|
103
|
+
} }), 'recall_private_message');
|
|
89
104
|
}
|
|
90
105
|
}
|
|
91
106
|
#admitRaw(event) {
|
|
107
|
+
if (event.self_id != null)
|
|
108
|
+
this[endpointSpec].reportSelfUserId(String(event.self_id));
|
|
92
109
|
const data = parseMessageReceiveData(event);
|
|
93
110
|
if (!data)
|
|
94
111
|
return;
|
|
@@ -98,14 +115,16 @@ export class MilkySseEndpoint extends ClientEndpoint {
|
|
|
98
115
|
this.#logger.warn(formatCompact({ op: 'milky_platform_event_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
99
116
|
}
|
|
100
117
|
#admitMessage(data, event) {
|
|
101
|
-
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
118
|
+
const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
|
|
102
119
|
const target = `${conversation.kind}:${conversation.id}`;
|
|
103
120
|
const content = formatInboundContent(data);
|
|
104
121
|
const segments = formatInboundSegments(data);
|
|
105
122
|
const audioUrl = extractInboundAudioUrl(data);
|
|
106
123
|
const nickname = senderNickname(data);
|
|
107
124
|
const mentioned = isMentioned(data, event.self_id);
|
|
108
|
-
void this.
|
|
125
|
+
void this[endpointSpec].events.message({
|
|
126
|
+
event,
|
|
127
|
+
...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
|
|
109
128
|
conversation,
|
|
110
129
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
111
130
|
content,
|
|
@@ -193,7 +212,7 @@ export class MilkySseEndpoint extends ClientEndpoint {
|
|
|
193
212
|
#onMessage(data) {
|
|
194
213
|
try {
|
|
195
214
|
const event = JSON.parse(data);
|
|
196
|
-
this.
|
|
215
|
+
this.ingest(event);
|
|
197
216
|
}
|
|
198
217
|
catch (error) {
|
|
199
218
|
this.#logger.warn(formatCompact({
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
|
|
2
2
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
3
3
|
import type { CapabilityId } from 'zhin.js';
|
|
4
|
-
import {
|
|
4
|
+
import { MilkyClient } from './client.js';
|
|
5
5
|
import { callApi, type MilkyWebhookConfig } from './protocol.js';
|
|
6
6
|
export interface MilkyWebhookEndpointOptions {
|
|
7
7
|
readonly id: CapabilityId;
|
|
8
|
+
readonly signal?: AbortSignal;
|
|
8
9
|
readonly http: HttpHost;
|
|
9
10
|
readonly config: MilkyWebhookConfig;
|
|
10
11
|
readonly callApi?: typeof callApi;
|
|
11
12
|
}
|
|
12
|
-
export declare class MilkyWebhookEndpoint extends
|
|
13
|
+
export declare class MilkyWebhookEndpoint extends MilkyClient implements Endpoint {
|
|
13
14
|
#private;
|
|
14
|
-
readonly
|
|
15
|
-
readonly management: EndpointManagement;
|
|
16
|
-
readonly control: EndpointControl;
|
|
15
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
17
16
|
constructor(options: MilkyWebhookEndpointOptions);
|
|
18
|
-
start(): Promise<void>;
|
|
19
|
-
stop(): Promise<void>;
|
|
20
|
-
send({ conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
21
|
-
recallMessage(id: string): Promise<void>;
|
|
22
17
|
}
|
package/lib/webhook-endpoint.js
CHANGED
|
@@ -1,33 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { acceptHttpIngress } from 'imhelper';
|
|
2
|
+
import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
2
3
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
3
4
|
import { verifyMilkyAccessToken } from './milky-auth.js';
|
|
4
5
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
5
|
-
import {
|
|
6
|
+
import { createMilkyActionCaller, readMilkyResponse, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
|
|
6
7
|
import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
|
|
7
|
-
export class MilkyWebhookEndpoint extends
|
|
8
|
-
|
|
8
|
+
export class MilkyWebhookEndpoint extends MilkyClient {
|
|
9
|
+
[endpointSpec];
|
|
10
|
+
#open = false;
|
|
9
11
|
#logger;
|
|
10
12
|
#options;
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
control = createRecallEndpointControl((id) => this
|
|
13
|
+
#management;
|
|
14
|
+
#call;
|
|
15
|
+
#control = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
|
|
14
16
|
#routeReleases = [];
|
|
15
17
|
#started = false;
|
|
16
18
|
constructor(options) {
|
|
17
|
-
|
|
19
|
+
const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
|
|
20
|
+
super(milkyClientOptions({ config: options.config, call }));
|
|
21
|
+
this.#call = call;
|
|
18
22
|
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
19
23
|
this.#options = options;
|
|
20
|
-
this.#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
this.#management = createMilkyEndpointManagement({
|
|
25
|
+
callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
|
|
26
|
+
});
|
|
27
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
28
|
+
name: options.config.id,
|
|
29
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
30
|
+
close: () => { this.#open = false; }, stop: () => this.#stop(),
|
|
31
|
+
send: request => this.#send(request),
|
|
32
|
+
management: this.#management, control: this.#control,
|
|
33
|
+
});
|
|
34
|
+
forwardMilkyClientEvents(this, (name, payload) => {
|
|
35
|
+
void this[endpointSpec].events.run(() => {
|
|
36
|
+
if (!this.#started)
|
|
37
|
+
return;
|
|
38
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
39
|
+
if (name === 'event')
|
|
40
|
+
this.#admitRaw(payload);
|
|
41
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
24
42
|
});
|
|
25
|
-
this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
|
|
26
|
-
if (name === 'event')
|
|
27
|
-
this.#admitRaw(payload);
|
|
28
|
-
}, (_name, error) => this.#warnPlatformEvent(error));
|
|
29
43
|
}
|
|
30
|
-
async start() {
|
|
44
|
+
async #start() {
|
|
31
45
|
if (this.#started)
|
|
32
46
|
return;
|
|
33
47
|
this.#started = true;
|
|
@@ -39,17 +53,17 @@ export class MilkyWebhookEndpoint extends ClientEndpoint {
|
|
|
39
53
|
path: this.#options.config.path,
|
|
40
54
|
}));
|
|
41
55
|
}
|
|
42
|
-
async stop() {
|
|
43
|
-
this
|
|
56
|
+
async #stop() {
|
|
57
|
+
this.#open = false;
|
|
44
58
|
for (const release of this.#routeReleases.splice(0))
|
|
45
59
|
release();
|
|
46
60
|
this.#started = false;
|
|
47
61
|
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
48
62
|
}
|
|
49
|
-
async send({ conversation, payload }) {
|
|
63
|
+
async #send({ conversation, payload, signal }) {
|
|
50
64
|
const message = formatOutboundSegments(payload);
|
|
51
65
|
const { action, params } = buildSendAction(conversation, message);
|
|
52
|
-
const data = await
|
|
66
|
+
const data = readMilkyResponse(await this.#call({ action, params, signal }), action);
|
|
53
67
|
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
54
68
|
this.#logger.debug(formatCompact({
|
|
55
69
|
op: 'milky_send',
|
|
@@ -59,24 +73,26 @@ export class MilkyWebhookEndpoint extends ClientEndpoint {
|
|
|
59
73
|
}));
|
|
60
74
|
return messageId;
|
|
61
75
|
}
|
|
62
|
-
async recallMessage(id) {
|
|
76
|
+
async #recallMessage({ id, signal }) {
|
|
63
77
|
const parsed = parseMilkyMessageId(id);
|
|
64
78
|
if (!parsed)
|
|
65
79
|
throw new Error(`Invalid message id: ${id}`);
|
|
66
80
|
if (parsed.message_scene === 'group') {
|
|
67
|
-
await
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
|
|
82
|
+
group_id: parsed.peer_id,
|
|
83
|
+
message_seq: parsed.message_seq,
|
|
84
|
+
} }), 'recall_group_message');
|
|
71
85
|
}
|
|
72
86
|
else {
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
|
|
88
|
+
user_id: parsed.peer_id,
|
|
89
|
+
message_seq: parsed.message_seq,
|
|
90
|
+
} }), 'recall_private_message');
|
|
77
91
|
}
|
|
78
92
|
}
|
|
79
93
|
#admitRaw(event) {
|
|
94
|
+
if (event.self_id != null)
|
|
95
|
+
this[endpointSpec].reportSelfUserId(String(event.self_id));
|
|
80
96
|
const data = parseMessageReceiveData(event);
|
|
81
97
|
if (!data)
|
|
82
98
|
return;
|
|
@@ -86,14 +102,16 @@ export class MilkyWebhookEndpoint extends ClientEndpoint {
|
|
|
86
102
|
this.#logger.warn(formatCompact({ op: 'milky_platform_event_failed', error: error instanceof Error ? error.message : String(error) }));
|
|
87
103
|
}
|
|
88
104
|
#admitMessage(data, event) {
|
|
89
|
-
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
105
|
+
const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
|
|
90
106
|
const target = `${conversation.kind}:${conversation.id}`;
|
|
91
107
|
const content = formatInboundContent(data);
|
|
92
108
|
const segments = formatInboundSegments(data);
|
|
93
109
|
const audioUrl = extractInboundAudioUrl(data);
|
|
94
110
|
const nickname = senderNickname(data);
|
|
95
111
|
const mentioned = isMentioned(data, event.self_id);
|
|
96
|
-
void this.
|
|
112
|
+
void this[endpointSpec].events.message({
|
|
113
|
+
event,
|
|
114
|
+
...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
|
|
97
115
|
conversation,
|
|
98
116
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
99
117
|
content,
|
|
@@ -131,12 +149,15 @@ export class MilkyWebhookEndpoint extends ClientEndpoint {
|
|
|
131
149
|
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
132
150
|
return;
|
|
133
151
|
}
|
|
134
|
-
if (!this
|
|
152
|
+
if (!this.#open) {
|
|
135
153
|
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
136
154
|
response.end(JSON.stringify({ status: 'ok' }));
|
|
137
155
|
return;
|
|
138
156
|
}
|
|
139
|
-
await
|
|
157
|
+
await acceptHttpIngress(request, response, event => {
|
|
158
|
+
void this[endpointSpec].events.run(() => this.ingest(event))
|
|
159
|
+
.catch(error => this.#warnPlatformEvent(error));
|
|
160
|
+
});
|
|
140
161
|
}
|
|
141
162
|
catch (error) {
|
|
142
163
|
this.#logger.error('Milky webhook error:', error);
|
package/lib/ws-endpoint.d.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
|
|
2
2
|
import type { CapabilityId } from 'zhin.js';
|
|
3
|
-
import {
|
|
3
|
+
import { MilkyClient } from './client.js';
|
|
4
4
|
import { callApi, type MilkyWsConfig } from './protocol.js';
|
|
5
5
|
import type { MilkyWsCreateOptions, MilkyWsSocket } from './ws-types.js';
|
|
6
6
|
export interface MilkyWsEndpointOptions {
|
|
7
7
|
readonly id: CapabilityId;
|
|
8
|
+
readonly signal?: AbortSignal;
|
|
8
9
|
readonly config: MilkyWsConfig;
|
|
9
10
|
readonly createWebSocket?: (url: string, options: MilkyWsCreateOptions) => MilkyWsSocket;
|
|
10
11
|
readonly callApi?: typeof callApi;
|
|
11
12
|
}
|
|
12
|
-
export declare class MilkyWsEndpoint extends
|
|
13
|
+
export declare class MilkyWsEndpoint extends MilkyClient implements Endpoint {
|
|
13
14
|
#private;
|
|
14
|
-
readonly
|
|
15
|
-
readonly management: EndpointManagement;
|
|
16
|
-
readonly control: EndpointControl;
|
|
15
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
17
16
|
constructor(options: MilkyWsEndpointOptions);
|
|
18
|
-
start(): Promise<void>;
|
|
19
|
-
close(): void;
|
|
20
|
-
stop(): Promise<void>;
|
|
21
|
-
send({ conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
22
|
-
recallMessage(id: string): Promise<void>;
|
|
23
17
|
}
|
package/lib/ws-endpoint.js
CHANGED
|
@@ -2,35 +2,50 @@
|
|
|
2
2
|
* Milky WS client endpoint — outbound connect to Milky protocol server.
|
|
3
3
|
*/
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
|
-
import {
|
|
5
|
+
import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
6
6
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
7
7
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
8
|
-
import {
|
|
8
|
+
import { createMilkyActionCaller, readMilkyResponse, milkyClientOptions, forwardMilkyClientEvents, MilkyClient, } from './client.js';
|
|
9
9
|
import { buildSendAction, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
|
|
10
10
|
const WS_OPEN = 1;
|
|
11
|
-
export class MilkyWsEndpoint extends
|
|
12
|
-
|
|
11
|
+
export class MilkyWsEndpoint extends MilkyClient {
|
|
12
|
+
[endpointSpec];
|
|
13
|
+
#open = false;
|
|
13
14
|
#logger;
|
|
14
15
|
#options;
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
control = createRecallEndpointControl((id) => this
|
|
16
|
+
#management;
|
|
17
|
+
#call;
|
|
18
|
+
#control = createRecallEndpointControl((id, signal) => this.#recallMessage({ id, signal }));
|
|
18
19
|
#lifecycle;
|
|
19
20
|
#ws;
|
|
20
21
|
#clientSocketRelease;
|
|
21
22
|
constructor(options) {
|
|
22
|
-
|
|
23
|
+
const call = createMilkyActionCaller({ config: options.config, request: options.callApi ?? callApi, signal: options.signal });
|
|
24
|
+
super(milkyClientOptions({ config: options.config, call }));
|
|
25
|
+
this.#call = call;
|
|
23
26
|
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
24
27
|
this.#options = options;
|
|
25
|
-
this.#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
this.#management = createMilkyEndpointManagement({
|
|
29
|
+
callApi: request => call(request).then(response => readMilkyResponse(response, request.action)),
|
|
30
|
+
});
|
|
31
|
+
const readTransportState = () => this.#lifecycle.state;
|
|
32
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
33
|
+
name: options.config.id,
|
|
34
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
35
|
+
close: () => { this.#close(); }, stop: () => this.#stop(),
|
|
36
|
+
send: request => this.#send(request),
|
|
37
|
+
management: this.#management, control: this.#control,
|
|
38
|
+
get transportState() { return readTransportState(); },
|
|
39
|
+
});
|
|
40
|
+
forwardMilkyClientEvents(this, (name, payload) => {
|
|
41
|
+
void this[endpointSpec].events.run(() => {
|
|
42
|
+
if (!this.#open && !this.#lifecycle.started)
|
|
43
|
+
return;
|
|
44
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
45
|
+
if (name === 'event')
|
|
46
|
+
this.#admitRaw(payload);
|
|
47
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
29
48
|
});
|
|
30
|
-
this.bindClientEvents((receive) => forwardMilkyClientEvents(this.client, receive), (name, payload) => {
|
|
31
|
-
if (name === 'event')
|
|
32
|
-
this.#admitRaw(payload);
|
|
33
|
-
}, (_name, error) => this.#warnPlatformEvent(error));
|
|
34
49
|
this.#lifecycle = createEndpointLifecycle({
|
|
35
50
|
name: options.config.id,
|
|
36
51
|
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
@@ -42,18 +57,18 @@ export class MilkyWsEndpoint extends ClientEndpoint {
|
|
|
42
57
|
},
|
|
43
58
|
});
|
|
44
59
|
}
|
|
45
|
-
async start() {
|
|
60
|
+
async #start() {
|
|
46
61
|
if (this.#lifecycle.started)
|
|
47
62
|
return;
|
|
48
63
|
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
49
64
|
}
|
|
50
|
-
close() {
|
|
51
|
-
|
|
65
|
+
#close() {
|
|
66
|
+
this.#open = false;
|
|
52
67
|
this.#clientSocketRelease?.();
|
|
53
68
|
this.#clientSocketRelease = undefined;
|
|
54
69
|
}
|
|
55
|
-
async stop() {
|
|
56
|
-
this
|
|
70
|
+
async #stop() {
|
|
71
|
+
this.#close();
|
|
57
72
|
await this.#lifecycle.stop();
|
|
58
73
|
if (this.#ws) {
|
|
59
74
|
try {
|
|
@@ -65,10 +80,10 @@ export class MilkyWsEndpoint extends ClientEndpoint {
|
|
|
65
80
|
this.#ws = undefined;
|
|
66
81
|
}
|
|
67
82
|
}
|
|
68
|
-
async send({ conversation, payload }) {
|
|
83
|
+
async #send({ conversation, payload, signal }) {
|
|
69
84
|
const message = formatOutboundSegments(payload);
|
|
70
85
|
const { action, params } = buildSendAction(conversation, message);
|
|
71
|
-
const data = await
|
|
86
|
+
const data = readMilkyResponse(await this.#call({ action, params, signal }), action);
|
|
72
87
|
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
73
88
|
this.#logger.debug(formatCompact({
|
|
74
89
|
op: 'milky_send',
|
|
@@ -78,24 +93,26 @@ export class MilkyWsEndpoint extends ClientEndpoint {
|
|
|
78
93
|
}));
|
|
79
94
|
return messageId;
|
|
80
95
|
}
|
|
81
|
-
async recallMessage(id) {
|
|
96
|
+
async #recallMessage({ id, signal }) {
|
|
82
97
|
const parsed = parseMilkyMessageId(id);
|
|
83
98
|
if (!parsed)
|
|
84
99
|
throw new Error(`Invalid message id: ${id}`);
|
|
85
100
|
if (parsed.message_scene === 'group') {
|
|
86
|
-
await
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
readMilkyResponse(await this.#call({ action: 'recall_group_message', signal, params: {
|
|
102
|
+
group_id: parsed.peer_id,
|
|
103
|
+
message_seq: parsed.message_seq,
|
|
104
|
+
} }), 'recall_group_message');
|
|
90
105
|
}
|
|
91
106
|
else {
|
|
92
|
-
await
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
readMilkyResponse(await this.#call({ action: 'recall_private_message', signal, params: {
|
|
108
|
+
user_id: parsed.peer_id,
|
|
109
|
+
message_seq: parsed.message_seq,
|
|
110
|
+
} }), 'recall_private_message');
|
|
96
111
|
}
|
|
97
112
|
}
|
|
98
113
|
#admitRaw(event) {
|
|
114
|
+
if (event.self_id != null)
|
|
115
|
+
this[endpointSpec].reportSelfUserId(String(event.self_id));
|
|
99
116
|
const data = parseMessageReceiveData(event);
|
|
100
117
|
if (!data)
|
|
101
118
|
return;
|
|
@@ -108,14 +125,16 @@ export class MilkyWsEndpoint extends ClientEndpoint {
|
|
|
108
125
|
}));
|
|
109
126
|
}
|
|
110
127
|
#admitMessage(data, event) {
|
|
111
|
-
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
128
|
+
const conversation = { ...milkyInboundConversation(String(this.#options.id), data), endpoint: this[endpointSpec].identity };
|
|
112
129
|
const target = `${conversation.kind}:${conversation.id}`;
|
|
113
130
|
const content = formatInboundContent(data);
|
|
114
131
|
const segments = formatInboundSegments(data);
|
|
115
132
|
const audioUrl = extractInboundAudioUrl(data);
|
|
116
133
|
const nickname = senderNickname(data);
|
|
117
134
|
const mentioned = isMentioned(data, event.self_id);
|
|
118
|
-
void this.
|
|
135
|
+
void this[endpointSpec].events.message({
|
|
136
|
+
event,
|
|
137
|
+
...(typeof event.time === 'number' ? { platformSentAt: event.time * 1000 } : {}),
|
|
119
138
|
conversation,
|
|
120
139
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
121
140
|
content,
|
|
@@ -187,7 +206,7 @@ export class MilkyWsEndpoint extends ClientEndpoint {
|
|
|
187
206
|
resolve();
|
|
188
207
|
});
|
|
189
208
|
this.#clientSocketRelease?.();
|
|
190
|
-
this.#clientSocketRelease = this.
|
|
209
|
+
this.#clientSocketRelease = this.acceptWebSocket(ws);
|
|
191
210
|
ws.on('close', (code, reason) => {
|
|
192
211
|
this.#clientSocketRelease?.();
|
|
193
212
|
this.#clientSocketRelease = undefined;
|
package/lib/wss-endpoint.d.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Milky reverse WSS endpoint — httpHostToken WS upgrade inbound + baseUrl HTTP API outbound.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
|
|
5
5
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
6
6
|
import type { CapabilityId } from 'zhin.js';
|
|
7
|
-
import {
|
|
7
|
+
import { MilkyClient } from './client.js';
|
|
8
8
|
import { callApi, type MilkyWssConfig } from './protocol.js';
|
|
9
9
|
export interface MilkyWssEndpointOptions {
|
|
10
10
|
readonly id: CapabilityId;
|
|
11
|
+
readonly signal?: AbortSignal;
|
|
11
12
|
readonly http: HttpHost;
|
|
12
13
|
readonly config: MilkyWssConfig;
|
|
13
14
|
readonly callApi?: typeof callApi;
|
|
14
15
|
}
|
|
15
|
-
export declare class MilkyWssEndpoint extends
|
|
16
|
+
export declare class MilkyWssEndpoint extends MilkyClient implements Endpoint {
|
|
16
17
|
#private;
|
|
17
|
-
readonly
|
|
18
|
-
readonly management: EndpointManagement;
|
|
19
|
-
readonly control: EndpointControl;
|
|
18
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
20
19
|
constructor(options: MilkyWssEndpointOptions);
|
|
21
|
-
start(): Promise<void>;
|
|
22
|
-
stop(): Promise<void>;
|
|
23
|
-
send({ conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
24
|
-
recallMessage(id: string): Promise<void>;
|
|
25
20
|
}
|