@zhin.js/adapter-milky 5.0.0 → 5.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.
- package/CHANGELOG.md +58 -0
- package/adapters/milky.ts +11 -0
- package/commands/endpoint/add/[name:string].ts +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[name:string].ts +3 -0
- package/lib/endpoint-management.d.ts +17 -0
- package/lib/endpoint-management.js +47 -0
- package/lib/milky-endpoint-commands.d.ts +1 -0
- package/lib/milky-endpoint-commands.js +18 -0
- package/lib/milky-runtime-state.d.ts +1 -0
- package/lib/milky-runtime-state.js +6 -0
- package/lib/protocol.d.ts +8 -0
- package/lib/protocol.js +121 -0
- package/lib/sse-endpoint.d.ts +2 -1
- package/lib/sse-endpoint.js +51 -42
- package/lib/webhook-endpoint.d.ts +2 -1
- package/lib/webhook-endpoint.js +5 -1
- package/lib/ws-endpoint.d.ts +2 -1
- package/lib/ws-endpoint.js +51 -57
- package/lib/wss-endpoint.d.ts +2 -1
- package/lib/wss-endpoint.js +5 -1
- package/package.json +18 -12
- package/plugin.ts +6 -0
- package/src/endpoint-management.ts +74 -0
- package/src/milky-endpoint-commands.ts +19 -0
- package/src/milky-runtime-state.ts +7 -0
- package/src/protocol.ts +120 -0
- package/src/sse-endpoint.ts +52 -41
- package/src/webhook-endpoint.ts +6 -1
- package/src/ws-endpoint.ts +53 -55
- package/src/wss-endpoint.ts +6 -1
package/src/sse-endpoint.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Milky SSE client endpoint — GET text/event-stream on /event.
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
createEndpointLifecycle,
|
|
6
|
+
type EndpointConnectHandle,
|
|
7
|
+
type EndpointInstance,
|
|
8
|
+
type EndpointLifecycle,
|
|
9
|
+
type EndpointManagement,
|
|
10
|
+
} from '@zhin.js/adapter';
|
|
5
11
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
6
12
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
13
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
14
|
+
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
8
15
|
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
9
16
|
import {
|
|
10
17
|
buildSendAction,
|
|
@@ -13,6 +20,7 @@ import {
|
|
|
13
20
|
extractInboundAudioUrl,
|
|
14
21
|
formatInboundContent,
|
|
15
22
|
formatInboundMessageId,
|
|
23
|
+
formatInboundSegments,
|
|
16
24
|
formatInboundTarget,
|
|
17
25
|
formatOutboundMessageId,
|
|
18
26
|
formatOutboundSegments,
|
|
@@ -47,24 +55,38 @@ export interface MilkySseEndpointOptions {
|
|
|
47
55
|
export class MilkySseEndpoint implements EndpointInstance {
|
|
48
56
|
readonly #options: MilkySseEndpointOptions;
|
|
49
57
|
readonly #callApi: typeof callApi;
|
|
58
|
+
readonly management: EndpointManagement = createMilkyEndpointManagement(this);
|
|
59
|
+
readonly #lifecycle: EndpointLifecycle;
|
|
50
60
|
#stream?: SseClientHandle;
|
|
51
|
-
#reconnectTimer?: NodeJS.Timeout;
|
|
52
61
|
#open = false;
|
|
53
|
-
#started = false;
|
|
54
|
-
#stopping = false;
|
|
55
62
|
#unregisterAgent?: () => void;
|
|
56
63
|
|
|
57
64
|
constructor(options: MilkySseEndpointOptions) {
|
|
58
65
|
this.#options = options;
|
|
59
66
|
this.#callApi = options.callApi ?? callApi;
|
|
67
|
+
this.#lifecycle = createEndpointLifecycle({
|
|
68
|
+
name: options.config.name,
|
|
69
|
+
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
70
|
+
reconnect: {
|
|
71
|
+
initialIntervalMs: options.config.reconnect_interval,
|
|
72
|
+
multiplier: 1,
|
|
73
|
+
maxIntervalMs: Number.MAX_SAFE_INTEGER,
|
|
74
|
+
jitterMs: 0,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
async start(): Promise<void> {
|
|
63
|
-
if (this.#started) return;
|
|
64
|
-
this.#started = true;
|
|
65
|
-
this.#stopping = false;
|
|
80
|
+
if (this.#lifecycle.started) return;
|
|
66
81
|
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
|
|
67
|
-
|
|
82
|
+
try {
|
|
83
|
+
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
84
|
+
} catch (err) {
|
|
85
|
+
// 与 WS 对齐:start 失败复位由基座保证;agent 反注册留在适配器侧,允许重试
|
|
86
|
+
this.#unregisterAgent?.();
|
|
87
|
+
this.#unregisterAgent = undefined;
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
open(): void {
|
|
@@ -77,17 +99,11 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
77
99
|
|
|
78
100
|
async stop(): Promise<void> {
|
|
79
101
|
this.#open = false;
|
|
80
|
-
this.#
|
|
81
|
-
this.#started = false;
|
|
102
|
+
await this.#lifecycle.stop();
|
|
82
103
|
this.#unregisterAgent?.();
|
|
83
104
|
this.#unregisterAgent = undefined;
|
|
84
|
-
if (this.#reconnectTimer) {
|
|
85
|
-
clearTimeout(this.#reconnectTimer);
|
|
86
|
-
this.#reconnectTimer = undefined;
|
|
87
|
-
}
|
|
88
105
|
this.#stream?.close();
|
|
89
106
|
this.#stream = undefined;
|
|
90
|
-
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name, mode: 'sse' }));
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
@@ -204,6 +220,7 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
204
220
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
205
221
|
const target = formatInboundTarget(data);
|
|
206
222
|
const content = formatInboundContent(data);
|
|
223
|
+
const segments = formatInboundSegments(data);
|
|
207
224
|
const audioUrl = extractInboundAudioUrl(data);
|
|
208
225
|
const nickname = senderNickname(data);
|
|
209
226
|
const mentioned = isMentioned(data, event.self_id);
|
|
@@ -211,6 +228,7 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
211
228
|
adapter: this.#options.id,
|
|
212
229
|
target,
|
|
213
230
|
content,
|
|
231
|
+
segments,
|
|
214
232
|
sender: String(data.sender_id),
|
|
215
233
|
id: formatInboundMessageId(data),
|
|
216
234
|
metadata: Object.freeze({
|
|
@@ -234,7 +252,7 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
234
252
|
});
|
|
235
253
|
}
|
|
236
254
|
|
|
237
|
-
async #connect(): Promise<void> {
|
|
255
|
+
async #connect(handle: EndpointConnectHandle): Promise<void> {
|
|
238
256
|
const { url, headers, safeUrl } = buildSseConnectOptions(this.#options.config);
|
|
239
257
|
const create = this.#options.createSseStream ?? ((opts) => openSseStream(opts));
|
|
240
258
|
|
|
@@ -268,19 +286,29 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
268
286
|
},
|
|
269
287
|
});
|
|
270
288
|
this.#stream = stream;
|
|
289
|
+
handle.onForceClose(() => {
|
|
290
|
+
try {
|
|
291
|
+
stream.close();
|
|
292
|
+
} catch {
|
|
293
|
+
/* ignore */
|
|
294
|
+
}
|
|
295
|
+
});
|
|
271
296
|
void stream.closed.then(() => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
297
|
+
// stop-during-connect 竞态由基座静默 settle(主动停止不算失败),此处仅对运行期断开告警
|
|
298
|
+
if (this.#lifecycle.state !== 'stopped') {
|
|
299
|
+
logger.warn(formatCompact({
|
|
300
|
+
op: 'disconnect',
|
|
301
|
+
endpoint: this.#options.config.name,
|
|
302
|
+
mode: 'sse',
|
|
303
|
+
reconnect_ms: this.#options.config.reconnect_interval,
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
// 基座语义:仅曾 open 的连接才武装重连;初始连接失败由 start() 的 catch 复位
|
|
307
|
+
handle.notifyClosed(new Error('Milky SSE closed'));
|
|
279
308
|
if (!settled) {
|
|
280
309
|
settled = true;
|
|
281
310
|
reject(new Error('Milky SSE closed before open'));
|
|
282
311
|
}
|
|
283
|
-
this.#scheduleReconnect();
|
|
284
312
|
});
|
|
285
313
|
});
|
|
286
314
|
}
|
|
@@ -298,21 +326,4 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
298
326
|
}));
|
|
299
327
|
}
|
|
300
328
|
}
|
|
301
|
-
|
|
302
|
-
#scheduleReconnect(): void {
|
|
303
|
-
if (this.#stopping || !this.#started || this.#reconnectTimer) return;
|
|
304
|
-
const delay = this.#options.config.reconnect_interval;
|
|
305
|
-
this.#reconnectTimer = setTimeout(() => {
|
|
306
|
-
this.#reconnectTimer = undefined;
|
|
307
|
-
void this.#connect().catch((err) => {
|
|
308
|
-
logger.warn(formatCompact({
|
|
309
|
-
op: 'reconnect',
|
|
310
|
-
endpoint: this.#options.config.name,
|
|
311
|
-
mode: 'sse',
|
|
312
|
-
ok: false,
|
|
313
|
-
error: err instanceof Error ? err.message : String(err),
|
|
314
|
-
}));
|
|
315
|
-
});
|
|
316
|
-
}, delay);
|
|
317
|
-
}
|
|
318
329
|
}
|
package/src/webhook-endpoint.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
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 type { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
|
|
6
6
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
7
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
9
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
10
|
import { readRequestBody, verifyMilkyAccessToken } from './milky-auth.js';
|
|
11
|
+
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
11
12
|
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
12
13
|
import {
|
|
13
14
|
buildSendAction,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
extractInboundAudioUrl,
|
|
16
17
|
formatInboundContent,
|
|
17
18
|
formatInboundMessageId,
|
|
19
|
+
formatInboundSegments,
|
|
18
20
|
formatInboundTarget,
|
|
19
21
|
formatOutboundMessageId,
|
|
20
22
|
formatOutboundSegments,
|
|
@@ -40,6 +42,7 @@ export interface MilkyWebhookEndpointOptions {
|
|
|
40
42
|
export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
41
43
|
readonly #options: MilkyWebhookEndpointOptions;
|
|
42
44
|
readonly #callApi: typeof callApi;
|
|
45
|
+
readonly management: EndpointManagement = createMilkyEndpointManagement(this);
|
|
43
46
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
44
47
|
#open = false;
|
|
45
48
|
#started = false;
|
|
@@ -193,6 +196,7 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
|
193
196
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
194
197
|
const target = formatInboundTarget(data);
|
|
195
198
|
const content = formatInboundContent(data);
|
|
199
|
+
const segments = formatInboundSegments(data);
|
|
196
200
|
const audioUrl = extractInboundAudioUrl(data);
|
|
197
201
|
const nickname = senderNickname(data);
|
|
198
202
|
const mentioned = isMentioned(data, event.self_id);
|
|
@@ -200,6 +204,7 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
|
200
204
|
adapter: this.#options.id,
|
|
201
205
|
target,
|
|
202
206
|
content,
|
|
207
|
+
segments,
|
|
203
208
|
sender: String(data.sender_id),
|
|
204
209
|
id: formatInboundMessageId(data),
|
|
205
210
|
metadata: Object.freeze({
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
* Milky WS client endpoint — outbound connect to Milky protocol server.
|
|
3
3
|
*/
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
createEndpointLifecycle,
|
|
7
|
+
type EndpointConnectHandle,
|
|
8
|
+
type EndpointInstance,
|
|
9
|
+
type EndpointLifecycle,
|
|
10
|
+
type EndpointManagement,
|
|
11
|
+
} from '@zhin.js/adapter';
|
|
7
12
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
8
13
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
14
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
15
|
+
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
10
16
|
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
11
17
|
import {
|
|
12
18
|
buildSendAction,
|
|
@@ -15,6 +21,7 @@ import {
|
|
|
15
21
|
extractInboundAudioUrl,
|
|
16
22
|
formatInboundContent,
|
|
17
23
|
formatInboundMessageId,
|
|
24
|
+
formatInboundSegments,
|
|
18
25
|
formatInboundTarget,
|
|
19
26
|
formatOutboundMessageId,
|
|
20
27
|
formatOutboundSegments,
|
|
@@ -45,25 +52,38 @@ export interface MilkyWsEndpointOptions {
|
|
|
45
52
|
export class MilkyWsEndpoint implements EndpointInstance {
|
|
46
53
|
readonly #options: MilkyWsEndpointOptions;
|
|
47
54
|
readonly #callApi: typeof callApi;
|
|
55
|
+
readonly management: EndpointManagement = createMilkyEndpointManagement(this);
|
|
56
|
+
readonly #lifecycle: EndpointLifecycle;
|
|
48
57
|
#ws?: MilkyWsSocket;
|
|
49
|
-
#reconnectTimer?: NodeJS.Timeout;
|
|
50
|
-
#heartbeatTimer?: NodeJS.Timeout;
|
|
51
58
|
#open = false;
|
|
52
|
-
#started = false;
|
|
53
|
-
#stopping = false;
|
|
54
59
|
#unregisterAgent?: () => void;
|
|
55
60
|
|
|
56
61
|
constructor(options: MilkyWsEndpointOptions) {
|
|
57
62
|
this.#options = options;
|
|
58
63
|
this.#callApi = options.callApi ?? callApi;
|
|
64
|
+
this.#lifecycle = createEndpointLifecycle({
|
|
65
|
+
name: options.config.name,
|
|
66
|
+
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
67
|
+
reconnect: {
|
|
68
|
+
initialIntervalMs: options.config.reconnect_interval,
|
|
69
|
+
multiplier: 1,
|
|
70
|
+
maxIntervalMs: Number.MAX_SAFE_INTEGER,
|
|
71
|
+
jitterMs: 0,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
async start(): Promise<void> {
|
|
62
|
-
if (this.#started) return;
|
|
63
|
-
this.#started = true;
|
|
64
|
-
this.#stopping = false;
|
|
77
|
+
if (this.#lifecycle.started) return;
|
|
65
78
|
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.name, this);
|
|
66
|
-
|
|
79
|
+
try {
|
|
80
|
+
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
// start 失败复位由基座保证;agent 注册/反注册是适配器专有依赖,留在适配器侧
|
|
83
|
+
this.#unregisterAgent?.();
|
|
84
|
+
this.#unregisterAgent = undefined;
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
open(): void {
|
|
@@ -76,18 +96,9 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
76
96
|
|
|
77
97
|
async stop(): Promise<void> {
|
|
78
98
|
this.#open = false;
|
|
79
|
-
this.#
|
|
80
|
-
this.#started = false;
|
|
99
|
+
await this.#lifecycle.stop();
|
|
81
100
|
this.#unregisterAgent?.();
|
|
82
101
|
this.#unregisterAgent = undefined;
|
|
83
|
-
if (this.#reconnectTimer) {
|
|
84
|
-
clearTimeout(this.#reconnectTimer);
|
|
85
|
-
this.#reconnectTimer = undefined;
|
|
86
|
-
}
|
|
87
|
-
if (this.#heartbeatTimer) {
|
|
88
|
-
clearInterval(this.#heartbeatTimer);
|
|
89
|
-
this.#heartbeatTimer = undefined;
|
|
90
|
-
}
|
|
91
102
|
if (this.#ws) {
|
|
92
103
|
try {
|
|
93
104
|
this.#ws.close();
|
|
@@ -96,7 +107,6 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
96
107
|
}
|
|
97
108
|
this.#ws = undefined;
|
|
98
109
|
}
|
|
99
|
-
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
@@ -214,6 +224,7 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
214
224
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
215
225
|
const target = formatInboundTarget(data);
|
|
216
226
|
const content = formatInboundContent(data);
|
|
227
|
+
const segments = formatInboundSegments(data);
|
|
217
228
|
const audioUrl = extractInboundAudioUrl(data);
|
|
218
229
|
const nickname = senderNickname(data);
|
|
219
230
|
const mentioned = isMentioned(data, event.self_id);
|
|
@@ -221,6 +232,7 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
221
232
|
adapter: this.#options.id,
|
|
222
233
|
target,
|
|
223
234
|
content,
|
|
235
|
+
segments,
|
|
224
236
|
sender: String(data.sender_id),
|
|
225
237
|
id: formatInboundMessageId(data),
|
|
226
238
|
metadata: Object.freeze({
|
|
@@ -244,7 +256,7 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
244
256
|
});
|
|
245
257
|
}
|
|
246
258
|
|
|
247
|
-
async #connect(): Promise<void> {
|
|
259
|
+
async #connect(handle: EndpointConnectHandle): Promise<void> {
|
|
248
260
|
const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
|
|
249
261
|
const create = this.#options.createWebSocket
|
|
250
262
|
?? ((connectUrl: string, options: MilkyWsCreateOptions) =>
|
|
@@ -254,6 +266,13 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
254
266
|
let settled = false;
|
|
255
267
|
const ws = create(url, { headers });
|
|
256
268
|
this.#ws = ws;
|
|
269
|
+
handle.onForceClose(() => {
|
|
270
|
+
try {
|
|
271
|
+
ws.close();
|
|
272
|
+
} catch {
|
|
273
|
+
/* ignore */
|
|
274
|
+
}
|
|
275
|
+
});
|
|
257
276
|
|
|
258
277
|
ws.on('open', () => {
|
|
259
278
|
if (settled) return;
|
|
@@ -270,7 +289,16 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
270
289
|
mode: 'ws',
|
|
271
290
|
url: safeUrl,
|
|
272
291
|
}));
|
|
273
|
-
|
|
292
|
+
// stop-during-connect 竞态:已停止则不再武装心跳(基座 stop 已清理定时器)
|
|
293
|
+
if (this.#lifecycle.started) {
|
|
294
|
+
this.#lifecycle.startHeartbeat(() => {
|
|
295
|
+
try {
|
|
296
|
+
if (ws.readyState === WS_OPEN) ws.ping?.();
|
|
297
|
+
} catch {
|
|
298
|
+
/* ignore */
|
|
299
|
+
}
|
|
300
|
+
}, this.#options.config.heartbeat_interval);
|
|
301
|
+
}
|
|
274
302
|
resolve();
|
|
275
303
|
});
|
|
276
304
|
|
|
@@ -297,11 +325,12 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
297
325
|
error: `${reasonStr || 'closed'}${codeHint}`,
|
|
298
326
|
reconnect_ms: this.#options.config.reconnect_interval,
|
|
299
327
|
}));
|
|
328
|
+
// 基座语义:仅曾 open 的连接才武装重连;初始连接失败由 start() 的 catch 复位
|
|
329
|
+
handle.notifyClosed(new Error(`Milky WS 关闭: ${codeNum} ${reasonStr}`));
|
|
300
330
|
if (!settled) {
|
|
301
331
|
settled = true;
|
|
302
332
|
reject(new Error(`Milky WS 关闭: ${codeNum} ${reasonStr}`));
|
|
303
333
|
}
|
|
304
|
-
this.#scheduleReconnect();
|
|
305
334
|
});
|
|
306
335
|
|
|
307
336
|
ws.on('error', (err) => {
|
|
@@ -339,35 +368,4 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
339
368
|
}));
|
|
340
369
|
}
|
|
341
370
|
}
|
|
342
|
-
|
|
343
|
-
#startHeartbeat(): void {
|
|
344
|
-
if (this.#heartbeatTimer) {
|
|
345
|
-
clearInterval(this.#heartbeatTimer);
|
|
346
|
-
}
|
|
347
|
-
const interval = this.#options.config.heartbeat_interval;
|
|
348
|
-
if (interval <= 0) return;
|
|
349
|
-
this.#heartbeatTimer = setInterval(() => {
|
|
350
|
-
try {
|
|
351
|
-
if (this.#ws?.readyState === WS_OPEN) this.#ws.ping?.();
|
|
352
|
-
} catch {
|
|
353
|
-
/* ignore */
|
|
354
|
-
}
|
|
355
|
-
}, interval);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
#scheduleReconnect(): void {
|
|
359
|
-
if (this.#stopping || !this.#started || this.#reconnectTimer) return;
|
|
360
|
-
const delay = this.#options.config.reconnect_interval;
|
|
361
|
-
this.#reconnectTimer = setTimeout(() => {
|
|
362
|
-
this.#reconnectTimer = undefined;
|
|
363
|
-
void this.#connect().catch((err) => {
|
|
364
|
-
logger.warn(formatCompact({
|
|
365
|
-
op: 'reconnect',
|
|
366
|
-
endpoint: this.#options.config.name,
|
|
367
|
-
ok: false,
|
|
368
|
-
error: err instanceof Error ? err.message : String(err),
|
|
369
|
-
}));
|
|
370
|
-
});
|
|
371
|
-
}, delay);
|
|
372
|
-
}
|
|
373
371
|
}
|
package/src/wss-endpoint.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Milky reverse WSS endpoint — httpHostToken WS upgrade inbound + baseUrl HTTP API outbound.
|
|
3
3
|
*/
|
|
4
4
|
import { clearInterval } from 'node:timers';
|
|
5
|
-
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
|
|
6
6
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
7
|
import type { HttpHost, WsConnection } from '@zhin.js/host-http';
|
|
8
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
9
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
10
|
import { verifyMilkyAccessToken } from './milky-auth.js';
|
|
11
|
+
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
11
12
|
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
12
13
|
import {
|
|
13
14
|
buildSendAction,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
extractInboundAudioUrl,
|
|
16
17
|
formatInboundContent,
|
|
17
18
|
formatInboundMessageId,
|
|
19
|
+
formatInboundSegments,
|
|
18
20
|
formatInboundTarget,
|
|
19
21
|
formatOutboundMessageId,
|
|
20
22
|
formatOutboundSegments,
|
|
@@ -42,6 +44,7 @@ export interface MilkyWssEndpointOptions {
|
|
|
42
44
|
export class MilkyWssEndpoint implements EndpointInstance {
|
|
43
45
|
readonly #options: MilkyWssEndpointOptions;
|
|
44
46
|
readonly #callApi: typeof callApi;
|
|
47
|
+
readonly management: EndpointManagement = createMilkyEndpointManagement(this);
|
|
45
48
|
#ws?: MilkyWsSocket;
|
|
46
49
|
#wsRelease?: () => void;
|
|
47
50
|
#heartbeatTimer?: NodeJS.Timeout;
|
|
@@ -212,6 +215,7 @@ export class MilkyWssEndpoint implements EndpointInstance {
|
|
|
212
215
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
213
216
|
const target = formatInboundTarget(data);
|
|
214
217
|
const content = formatInboundContent(data);
|
|
218
|
+
const segments = formatInboundSegments(data);
|
|
215
219
|
const audioUrl = extractInboundAudioUrl(data);
|
|
216
220
|
const nickname = senderNickname(data);
|
|
217
221
|
const mentioned = isMentioned(data, event.self_id);
|
|
@@ -219,6 +223,7 @@ export class MilkyWssEndpoint implements EndpointInstance {
|
|
|
219
223
|
adapter: this.#options.id,
|
|
220
224
|
target,
|
|
221
225
|
content,
|
|
226
|
+
segments,
|
|
222
227
|
sender: String(data.sender_id),
|
|
223
228
|
id: formatInboundMessageId(data),
|
|
224
229
|
metadata: Object.freeze({
|