@zhin.js/adapter-milky 1.1.25 → 1.1.27
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 +155 -0
- package/LICENSE +21 -0
- package/PERMITS.md +3 -3
- package/README.md +6 -3
- package/adapters/milky/index.js +3 -0
- package/adapters/milky/index.ts +3 -0
- package/lib/console-spec.js +33 -3
- package/lib/endpoint-management.js +37 -0
- package/lib/milky-auth.d.ts +2 -2
- package/lib/milky-auth.js +3 -12
- package/lib/protocol.d.ts +6 -1
- package/lib/protocol.js +10 -0
- package/lib/sse-client.d.ts +6 -0
- package/lib/sse-client.js +41 -2
- package/lib/sse-endpoint.js +3 -2
- package/lib/webhook-endpoint.js +13 -17
- package/lib/ws-endpoint.js +3 -2
- package/lib/ws-types.d.ts +6 -0
- package/lib/ws-types.js +31 -1
- package/lib/wss-endpoint.d.ts +1 -1
- package/lib/wss-endpoint.js +21 -17
- package/package.json +17 -33
- package/schema.json +0 -56
- package/src/console-spec.ts +33 -3
- package/src/endpoint-management.ts +33 -0
- package/src/milky-auth.ts +4 -11
- package/src/protocol.ts +11 -1
- package/src/sse-client.ts +45 -2
- package/src/sse-endpoint.ts +3 -1
- package/src/webhook-endpoint.ts +10 -17
- package/src/ws-endpoint.ts +3 -1
- package/src/ws-types.ts +33 -0
- package/src/wss-endpoint.ts +21 -16
package/src/webhook-endpoint.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { createMilkySelfProfile } from './self-profile.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Milky webhook endpoint — httpHostToken POST inbound + baseUrl HTTP API outbound.
|
|
4
4
|
*/
|
|
5
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
6
5
|
import { acceptHttpIngress } from 'imhelper';
|
|
7
6
|
import {
|
|
8
7
|
createEndpointSpecification, endpointSpec, type Endpoint, type EndpointSpecification,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
formatOutboundSegments,
|
|
29
28
|
isMentioned,
|
|
30
29
|
milkyInboundConversation,
|
|
30
|
+
milkySenderStanding,
|
|
31
31
|
parseMessageReceiveData,
|
|
32
32
|
parseMilkyMessageId,
|
|
33
33
|
senderNickname,
|
|
@@ -150,6 +150,7 @@ export class MilkyWebhookEndpoint extends MilkyClient implements Endpoint {
|
|
|
150
150
|
const segments = formatInboundSegments(data);
|
|
151
151
|
const audioUrl = extractInboundAudioUrl(data);
|
|
152
152
|
const nickname = senderNickname(data);
|
|
153
|
+
const platform = milkySenderStanding(data);
|
|
153
154
|
const mentioned = isMentioned(data, event.self_id);
|
|
154
155
|
void this[endpointSpec].events.message({
|
|
155
156
|
event,
|
|
@@ -158,7 +159,7 @@ export class MilkyWebhookEndpoint extends MilkyClient implements Endpoint {
|
|
|
158
159
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
159
160
|
content,
|
|
160
161
|
segments,
|
|
161
|
-
sender: { id: String(data.sender_id), name: nickname },
|
|
162
|
+
sender: { id: String(data.sender_id), name: nickname, ...(platform ? { platform } : {}) },
|
|
162
163
|
endpointId: this.#options.config.id,
|
|
163
164
|
...(mentioned ? { mentioned: true } : {}),
|
|
164
165
|
metadata: Object.freeze({
|
|
@@ -182,34 +183,26 @@ export class MilkyWebhookEndpoint extends MilkyClient implements Endpoint {
|
|
|
182
183
|
#setupRoutes(): void {
|
|
183
184
|
const path = this.#options.config.path;
|
|
184
185
|
this.#routeReleases.push(
|
|
185
|
-
this.#options.http.route('POST', path,
|
|
186
|
-
await this.#handleWebhook(request, response);
|
|
187
|
-
}, { summary: 'Milky webhook callback', tags: ['milky'] }),
|
|
186
|
+
this.#options.http.route('POST', path, (request) => this.#handleWebhook(request), { summary: 'Milky webhook callback', tags: ['milky'] }),
|
|
188
187
|
);
|
|
189
188
|
}
|
|
190
189
|
|
|
191
|
-
async #handleWebhook(request:
|
|
190
|
+
async #handleWebhook(request: Request): Promise<Response> {
|
|
192
191
|
try {
|
|
193
192
|
if (!verifyMilkyAccessToken(this.#options.config.access_token, request)) {
|
|
194
|
-
|
|
195
|
-
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
196
|
-
return;
|
|
193
|
+
return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
|
|
197
194
|
}
|
|
198
195
|
if (!this.#open) {
|
|
199
|
-
|
|
200
|
-
response.end(JSON.stringify({ status: 'ok' }));
|
|
201
|
-
return;
|
|
196
|
+
return new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
|
202
197
|
}
|
|
203
|
-
await acceptHttpIngress<MilkyEvent>(request,
|
|
198
|
+
const result = await acceptHttpIngress<MilkyEvent>(request, undefined, event => {
|
|
204
199
|
void this[endpointSpec].events.run(() => this.ingest(event as Parameters<MilkyClient['ingest']>[0]))
|
|
205
200
|
.catch(error => this.#warnPlatformEvent(error));
|
|
206
201
|
});
|
|
202
|
+
return new Response(JSON.stringify(result.body), { status: result.status, headers: result.headers });
|
|
207
203
|
} catch (error) {
|
|
208
204
|
this.#logger.error('Milky webhook error:', error);
|
|
209
|
-
|
|
210
|
-
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
211
|
-
response.end(JSON.stringify({ message: 'Internal Server Error' }));
|
|
212
|
-
}
|
|
205
|
+
return new Response(JSON.stringify({ message: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
|
|
213
206
|
}
|
|
214
207
|
}
|
|
215
208
|
}
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
formatOutboundSegments,
|
|
35
35
|
isMentioned,
|
|
36
36
|
milkyInboundConversation,
|
|
37
|
+
milkySenderStanding,
|
|
37
38
|
parseMessageReceiveData,
|
|
38
39
|
parseMilkyMessageId,
|
|
39
40
|
senderNickname,
|
|
@@ -183,6 +184,7 @@ export class MilkyWsEndpoint extends MilkyClient implements Endpoint {
|
|
|
183
184
|
const segments = formatInboundSegments(data);
|
|
184
185
|
const audioUrl = extractInboundAudioUrl(data);
|
|
185
186
|
const nickname = senderNickname(data);
|
|
187
|
+
const platform = milkySenderStanding(data);
|
|
186
188
|
const mentioned = isMentioned(data, event.self_id);
|
|
187
189
|
void this[endpointSpec].events.message({
|
|
188
190
|
event,
|
|
@@ -191,7 +193,7 @@ export class MilkyWsEndpoint extends MilkyClient implements Endpoint {
|
|
|
191
193
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
192
194
|
content,
|
|
193
195
|
segments,
|
|
194
|
-
sender: { id: String(data.sender_id), name: nickname },
|
|
196
|
+
sender: { id: String(data.sender_id), name: nickname, ...(platform ? { platform } : {}) },
|
|
195
197
|
endpointId: this.#options.config.id,
|
|
196
198
|
...(mentioned ? { mentioned: true } : {}),
|
|
197
199
|
metadata: Object.freeze({
|
package/src/ws-types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { WsConnection } from '@zhin.js/host-http';
|
|
2
|
+
|
|
1
3
|
/** Minimal WS surface used by the endpoint (real `ws` or test mock). */
|
|
2
4
|
export interface MilkyWsSocket {
|
|
3
5
|
readonly readyState: number;
|
|
@@ -10,3 +12,34 @@ export interface MilkyWsSocket {
|
|
|
10
12
|
export interface MilkyWsCreateOptions {
|
|
11
13
|
readonly headers?: Record<string, string>;
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A Host reverse-WebSocket connection seen through the MilkyWsSocket surface. Protocol
|
|
18
|
+
* keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
|
|
19
|
+
*/
|
|
20
|
+
export function socketOfConnection(connection: WsConnection): MilkyWsSocket {
|
|
21
|
+
let readyState = 1;
|
|
22
|
+
connection.onClose(() => { readyState = 3; });
|
|
23
|
+
const subscriptions = new Map<(...args: unknown[]) => void, () => void>();
|
|
24
|
+
const off = (_event: string, listener: (...args: unknown[]) => void): void => {
|
|
25
|
+
subscriptions.get(listener)?.();
|
|
26
|
+
subscriptions.delete(listener);
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
get readyState() { return readyState; },
|
|
30
|
+
send: (data: string) => connection.send(data),
|
|
31
|
+
close: (code?: number, reason?: string) => connection.close(code, reason),
|
|
32
|
+
on(event: string, listener: (...args: unknown[]) => void) {
|
|
33
|
+
if (event === 'message') {
|
|
34
|
+
subscriptions.set(listener, connection.onMessage((data) => {
|
|
35
|
+
listener(typeof data === 'string' ? data : Buffer.from(data));
|
|
36
|
+
}));
|
|
37
|
+
} else if (event === 'close') {
|
|
38
|
+
subscriptions.set(listener, connection.onClose((code, reason) => listener(code, reason)));
|
|
39
|
+
}
|
|
40
|
+
// 'error' surfaces as a close on a WsConnection; 'open' already happened.
|
|
41
|
+
},
|
|
42
|
+
off,
|
|
43
|
+
removeListener: off,
|
|
44
|
+
} as MilkyWsSocket;
|
|
45
|
+
}
|
package/src/wss-endpoint.ts
CHANGED
|
@@ -11,10 +11,9 @@ import {
|
|
|
11
11
|
type EndpointManagement,
|
|
12
12
|
type EndpointSendRequest,
|
|
13
13
|
} from 'zhin.js/adapter';
|
|
14
|
-
import type
|
|
14
|
+
import { verifyProtocolAccessToken, type HttpHost, type WsConnection } from '@zhin.js/host-http';
|
|
15
15
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
16
16
|
import type { CapabilityId } from 'zhin.js';
|
|
17
|
-
import { verifyMilkyAccessToken } from './milky-auth.js';
|
|
18
17
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
19
18
|
import { createMilkyActionCaller, readMilkyResponse, type MilkyActionCaller, milkyClientOptions, forwardMilkyClientEvents, MilkyClient } from './client.js';
|
|
20
19
|
import {
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
formatOutboundSegments,
|
|
29
28
|
isMentioned,
|
|
30
29
|
milkyInboundConversation,
|
|
30
|
+
milkySenderStanding,
|
|
31
31
|
parseMessageReceiveData,
|
|
32
32
|
parseMilkyMessageId,
|
|
33
33
|
senderNickname,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
type MilkyIncomingMessage,
|
|
36
36
|
type MilkyWssConfig,
|
|
37
37
|
} from './protocol.js';
|
|
38
|
-
import type
|
|
38
|
+
import { socketOfConnection, type MilkyWsSocket } from './ws-types.js';
|
|
39
39
|
|
|
40
40
|
const WS_OPEN = 1;
|
|
41
41
|
|
|
@@ -99,7 +99,17 @@ export class MilkyWssEndpoint extends MilkyClient implements Endpoint {
|
|
|
99
99
|
async #start(): Promise<void> {
|
|
100
100
|
if (this.#started) return;
|
|
101
101
|
this.#started = true;
|
|
102
|
-
|
|
102
|
+
if (!this.#options.config.access_token) {
|
|
103
|
+
// 未配 access_token 时只接受本机回环连接(verifyProtocolAccessToken)。
|
|
104
|
+
this.#logger.warn(formatCompact({
|
|
105
|
+
endpoint: this.#options.config.id,
|
|
106
|
+
mode: 'wss',
|
|
107
|
+
ok: false,
|
|
108
|
+
error: 'missing access_token: only loopback peers are accepted',
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
// The peer authenticates with this endpoint's own access_token, not a Console credential.
|
|
112
|
+
const handle = this.#options.http.ws(this.#options.config.path, { auth: 'protocol' });
|
|
103
113
|
this.#wsRelease = handle.onConnection((connection) => {
|
|
104
114
|
this.#connectionTask = this.#connectionTask
|
|
105
115
|
.then(() => this.#acceptConnection(connection))
|
|
@@ -177,6 +187,7 @@ export class MilkyWssEndpoint extends MilkyClient implements Endpoint {
|
|
|
177
187
|
const segments = formatInboundSegments(data);
|
|
178
188
|
const audioUrl = extractInboundAudioUrl(data);
|
|
179
189
|
const nickname = senderNickname(data);
|
|
190
|
+
const platform = milkySenderStanding(data);
|
|
180
191
|
const mentioned = isMentioned(data, event.self_id);
|
|
181
192
|
void this[endpointSpec].events.message({
|
|
182
193
|
event,
|
|
@@ -185,7 +196,7 @@ export class MilkyWssEndpoint extends MilkyClient implements Endpoint {
|
|
|
185
196
|
message: { conversation, id: formatInboundMessageId(data) },
|
|
186
197
|
content,
|
|
187
198
|
segments,
|
|
188
|
-
sender: { id: String(data.sender_id) },
|
|
199
|
+
sender: { id: String(data.sender_id), ...(platform ? { platform } : {}) },
|
|
189
200
|
endpointId: this.#options.config.id,
|
|
190
201
|
...(mentioned ? { mentioned: true } : {}),
|
|
191
202
|
metadata: Object.freeze({
|
|
@@ -207,11 +218,11 @@ export class MilkyWssEndpoint extends MilkyClient implements Endpoint {
|
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
async #acceptConnection(connection: WsConnection): Promise<void> {
|
|
210
|
-
if (!
|
|
211
|
-
connection.
|
|
221
|
+
if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
|
|
222
|
+
connection.close(4003, 'Unauthorized');
|
|
212
223
|
return;
|
|
213
224
|
}
|
|
214
|
-
const socket = connection
|
|
225
|
+
const socket = socketOfConnection(connection);
|
|
215
226
|
await this.#connectionLifecycle.stop();
|
|
216
227
|
await this.#connectionLifecycle.start(async (lifecycleHandle) => {
|
|
217
228
|
this.#ws = socket;
|
|
@@ -229,13 +240,7 @@ export class MilkyWssEndpoint extends MilkyClient implements Endpoint {
|
|
|
229
240
|
}
|
|
230
241
|
if (this.#ws === socket) this.#ws = undefined;
|
|
231
242
|
});
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
if (socket.readyState === WS_OPEN) socket.ping?.();
|
|
235
|
-
} catch {
|
|
236
|
-
/* ignore */
|
|
237
|
-
}
|
|
238
|
-
}, this.#options.config.heartbeat_interval);
|
|
243
|
+
// Dead-peer detection is the Host transport's keepalive (protocol ping / pong).
|
|
239
244
|
socket.on('message', () => {
|
|
240
245
|
this.#connectionLifecycle.notifyHeartbeatAck();
|
|
241
246
|
});
|
|
@@ -251,7 +256,7 @@ export class MilkyWssEndpoint extends MilkyClient implements Endpoint {
|
|
|
251
256
|
this.#logger.debug(formatCompact({
|
|
252
257
|
endpoint: this.#options.config.id,
|
|
253
258
|
mode: 'wss',
|
|
254
|
-
peer: connection.
|
|
259
|
+
peer: connection.remoteAddress,
|
|
255
260
|
}));
|
|
256
261
|
}
|
|
257
262
|
|