@zhin.js/adapter-milky 6.0.2 → 6.0.5
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 +86 -0
- package/README.md +3 -3
- package/adapters/milky.js +2 -2
- package/adapters/milky.ts +2 -2
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/milky-agent-deps.d.ts +2 -2
- package/lib/milky-agent-deps.js +7 -7
- package/lib/protocol.d.ts +15 -13
- package/lib/protocol.js +27 -30
- package/lib/sse-endpoint.d.ts +2 -5
- package/lib/sse-endpoint.js +27 -28
- package/lib/webhook-endpoint.d.ts +2 -5
- package/lib/webhook-endpoint.js +23 -23
- package/lib/ws-endpoint.d.ts +2 -5
- package/lib/ws-endpoint.js +29 -30
- package/lib/wss-endpoint.d.ts +2 -5
- package/lib/wss-endpoint.js +25 -25
- package/package.json +15 -14
- package/schema.json +41 -5
- package/src/index.ts +1 -6
- package/src/milky-agent-deps.ts +8 -8
- package/src/protocol.ts +34 -37
- package/src/sse-endpoint.ts +30 -30
- package/src/webhook-endpoint.ts +29 -25
- package/src/ws-endpoint.ts +31 -30
- package/src/wss-endpoint.ts +31 -26
- /package/commands/endpoint/add/{[name:string].js → [id].js} +0 -0
- /package/commands/endpoint/add/{[name:string].ts → [id].ts} +0 -0
- /package/commands/endpoint/remove/{[name:string].js → [id].js} +0 -0
- /package/commands/endpoint/remove/{[name:string].ts → [id].ts} +0 -0
package/src/protocol.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { isMediaRef } from '@zhin.js/core';
|
|
8
8
|
import type { Segment } from '@zhin.js/core/runtime';
|
|
9
|
+
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
9
10
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
10
11
|
|
|
11
12
|
const logger = getLogger('milky');
|
|
@@ -14,7 +15,7 @@ const logger = getLogger('milky');
|
|
|
14
15
|
export interface MilkyLegacyEndpointRow {
|
|
15
16
|
readonly context?: string;
|
|
16
17
|
readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
|
|
17
|
-
readonly
|
|
18
|
+
readonly id?: string;
|
|
18
19
|
readonly baseUrl?: string;
|
|
19
20
|
readonly access_token?: string;
|
|
20
21
|
readonly path?: string;
|
|
@@ -25,7 +26,7 @@ export interface MilkyLegacyEndpointRow {
|
|
|
25
26
|
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
26
27
|
export interface MilkyAdapterConfig {
|
|
27
28
|
readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
|
|
28
|
-
readonly
|
|
29
|
+
readonly id?: string;
|
|
29
30
|
readonly baseUrl?: string;
|
|
30
31
|
readonly access_token?: string;
|
|
31
32
|
readonly path?: string;
|
|
@@ -37,7 +38,7 @@ export interface MilkyAdapterConfig {
|
|
|
37
38
|
|
|
38
39
|
export interface MilkyConfigBase {
|
|
39
40
|
readonly context: 'milky';
|
|
40
|
-
readonly
|
|
41
|
+
readonly id: string;
|
|
41
42
|
readonly baseUrl: string;
|
|
42
43
|
readonly access_token?: string;
|
|
43
44
|
}
|
|
@@ -116,11 +117,6 @@ export interface MilkyOutgoingSegment {
|
|
|
116
117
|
data: Record<string, unknown>;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
export interface ParsedSendTarget {
|
|
120
|
-
readonly message_type: 'private' | 'group';
|
|
121
|
-
readonly id: string;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
120
|
export interface MilkyApiClientOptions {
|
|
125
121
|
readonly baseUrl: string;
|
|
126
122
|
readonly access_token?: string;
|
|
@@ -134,8 +130,8 @@ function normalizeConnection(connection: string | undefined): 'ws' | 'sse' | 'we
|
|
|
134
130
|
export function resolveMilkyConfig(config: MilkyAdapterConfig = {}): ResolvedMilkyConfig {
|
|
135
131
|
const entry = config.endpoints?.find((item) => item.context === 'milky');
|
|
136
132
|
const connection = normalizeConnection(config.connection ?? entry?.connection);
|
|
137
|
-
const
|
|
138
|
-
|| (typeof entry?.
|
|
133
|
+
const id = (typeof config.id === 'string' && config.id)
|
|
134
|
+
|| (typeof entry?.id === 'string' && entry.id)
|
|
139
135
|
|| process.env.MILKY_BOT_NAME
|
|
140
136
|
|| 'milky-bot';
|
|
141
137
|
const baseUrl = config.baseUrl ?? entry?.baseUrl;
|
|
@@ -147,7 +143,7 @@ export function resolveMilkyConfig(config: MilkyAdapterConfig = {}): ResolvedMil
|
|
|
147
143
|
const access_token = config.access_token ?? entry?.access_token;
|
|
148
144
|
const base = {
|
|
149
145
|
context: 'milky' as const,
|
|
150
|
-
|
|
146
|
+
id,
|
|
151
147
|
baseUrl,
|
|
152
148
|
access_token,
|
|
153
149
|
};
|
|
@@ -261,10 +257,22 @@ export function isMessageReceiveEvent(
|
|
|
261
257
|
return parseMessageReceiveData(event) != null;
|
|
262
258
|
}
|
|
263
259
|
|
|
264
|
-
/**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
260
|
+
/**
|
|
261
|
+
* 入站归一化 → ConversationRef:`friend`(私聊)→ `private`;`group` → `group`;
|
|
262
|
+
* `temp`(群临时会话)→ 群容器内的 `private` 会话(parent = 来源群)。
|
|
263
|
+
*/
|
|
264
|
+
export function milkyInboundConversation(
|
|
265
|
+
endpointKey: string,
|
|
266
|
+
data: MilkyIncomingMessage,
|
|
267
|
+
): ConversationRef {
|
|
268
|
+
return {
|
|
269
|
+
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
270
|
+
kind: data.message_scene === 'group' ? 'group' : 'private',
|
|
271
|
+
id: String(data.peer_id),
|
|
272
|
+
...(data.message_scene === 'temp' && data.group
|
|
273
|
+
? { parent: { kind: 'group' as const, id: String(data.group.group_id) } }
|
|
274
|
+
: {}),
|
|
275
|
+
};
|
|
268
276
|
}
|
|
269
277
|
|
|
270
278
|
export function formatInboundContent(data: MilkyIncomingMessage): string {
|
|
@@ -427,18 +435,6 @@ export function formatInboundMessageId(data: MilkyIncomingMessage): string {
|
|
|
427
435
|
return `${data.message_scene}:${data.peer_id}:${data.message_seq}`;
|
|
428
436
|
}
|
|
429
437
|
|
|
430
|
-
export function parseSendTarget(target: string): ParsedSendTarget {
|
|
431
|
-
const sep = target.indexOf(':');
|
|
432
|
-
if (sep <= 0) {
|
|
433
|
-
return { message_type: 'private', id: target };
|
|
434
|
-
}
|
|
435
|
-
const head = target.slice(0, sep);
|
|
436
|
-
const rest = target.slice(sep + 1);
|
|
437
|
-
if (head === 'group') return { message_type: 'group', id: rest };
|
|
438
|
-
if (head === 'private') return { message_type: 'private', id: rest };
|
|
439
|
-
return { message_type: 'private', id: target };
|
|
440
|
-
}
|
|
441
|
-
|
|
442
438
|
/**
|
|
443
439
|
* 解析 image/audio/video 段的投递 uri(canonical MediaRef 唯一来源):
|
|
444
440
|
* - kind=url → http(s):// 直发;
|
|
@@ -570,15 +566,14 @@ export function formatOutboundSegments(payload: unknown): MilkyOutgoingSegment[]
|
|
|
570
566
|
}
|
|
571
567
|
|
|
572
568
|
export function buildSendAction(
|
|
573
|
-
|
|
569
|
+
conversation: ConversationRef,
|
|
574
570
|
message: MilkyOutgoingSegment[],
|
|
575
571
|
): { action: string; params: Record<string, unknown> } {
|
|
576
|
-
|
|
577
|
-
if (parsed.message_type === 'group') {
|
|
572
|
+
if (conversation.kind === 'group') {
|
|
578
573
|
return {
|
|
579
574
|
action: 'send_group_message',
|
|
580
575
|
params: {
|
|
581
|
-
group_id: parseInt(
|
|
576
|
+
group_id: parseInt(conversation.id, 10),
|
|
582
577
|
message,
|
|
583
578
|
},
|
|
584
579
|
};
|
|
@@ -586,21 +581,23 @@ export function buildSendAction(
|
|
|
586
581
|
return {
|
|
587
582
|
action: 'send_private_message',
|
|
588
583
|
params: {
|
|
589
|
-
user_id: parseInt(
|
|
584
|
+
user_id: parseInt(conversation.id, 10),
|
|
590
585
|
message,
|
|
591
586
|
},
|
|
592
587
|
};
|
|
593
588
|
}
|
|
594
589
|
|
|
595
|
-
/**
|
|
590
|
+
/**
|
|
591
|
+
* message_seq result → gateway message id(`scene:peer:seq` 复合格式,
|
|
592
|
+
* 与 formatInboundMessageId 同源,仅供 recall 链在本端点边界内解析)。
|
|
593
|
+
*/
|
|
596
594
|
export function formatOutboundMessageId(
|
|
597
|
-
|
|
595
|
+
conversation: ConversationRef,
|
|
598
596
|
messageSeq: number | undefined,
|
|
599
597
|
): string {
|
|
600
598
|
if (messageSeq == null) return '';
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
return `${scene}:${parsed.id}:${messageSeq}`;
|
|
599
|
+
const scene = conversation.kind === 'group' ? 'group' : 'friend';
|
|
600
|
+
return `${scene}:${conversation.id}:${messageSeq}`;
|
|
604
601
|
}
|
|
605
602
|
|
|
606
603
|
export function parseMilkyMessageId(
|
package/src/sse-endpoint.ts
CHANGED
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
type EndpointInstance,
|
|
8
8
|
type EndpointLifecycle,
|
|
9
9
|
type EndpointManagement,
|
|
10
|
+
type EndpointSendRequest,
|
|
10
11
|
} from '@zhin.js/adapter';
|
|
11
12
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
12
|
-
import { formatCompact,
|
|
13
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
13
14
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
14
15
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
15
16
|
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
@@ -21,10 +22,10 @@ import {
|
|
|
21
22
|
formatInboundContent,
|
|
22
23
|
formatInboundMessageId,
|
|
23
24
|
formatInboundSegments,
|
|
24
|
-
formatInboundTarget,
|
|
25
25
|
formatOutboundMessageId,
|
|
26
26
|
formatOutboundSegments,
|
|
27
27
|
isMentioned,
|
|
28
|
+
milkyInboundConversation,
|
|
28
29
|
parseMessageReceiveData,
|
|
29
30
|
parseMilkyMessageId,
|
|
30
31
|
senderNickname,
|
|
@@ -34,8 +35,6 @@ import {
|
|
|
34
35
|
} from './protocol.js';
|
|
35
36
|
import { openSseStream, type SseClientHandle } from './sse-client.js';
|
|
36
37
|
|
|
37
|
-
const logger = getLogger('milky');
|
|
38
|
-
|
|
39
38
|
export type CreateMilkySseStream = (options: {
|
|
40
39
|
readonly url: string;
|
|
41
40
|
readonly headers: Record<string, string>;
|
|
@@ -53,6 +52,8 @@ export interface MilkySseEndpointOptions {
|
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
export class MilkySseEndpoint implements EndpointInstance {
|
|
55
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
56
|
+
|
|
56
57
|
readonly #options: MilkySseEndpointOptions;
|
|
57
58
|
readonly #callApi: typeof callApi;
|
|
58
59
|
readonly management: EndpointManagement = createMilkyEndpointManagement(this);
|
|
@@ -62,10 +63,11 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
62
63
|
#unregisterAgent?: () => void;
|
|
63
64
|
|
|
64
65
|
constructor(options: MilkySseEndpointOptions) {
|
|
66
|
+
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
65
67
|
this.#options = options;
|
|
66
68
|
this.#callApi = options.callApi ?? callApi;
|
|
67
69
|
this.#lifecycle = createEndpointLifecycle({
|
|
68
|
-
name: options.config.
|
|
70
|
+
name: options.config.id,
|
|
69
71
|
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
70
72
|
reconnect: {
|
|
71
73
|
initialIntervalMs: options.config.reconnect_interval,
|
|
@@ -78,7 +80,7 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
78
80
|
|
|
79
81
|
async start(): Promise<void> {
|
|
80
82
|
if (this.#lifecycle.started) return;
|
|
81
|
-
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.
|
|
83
|
+
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
|
|
82
84
|
try {
|
|
83
85
|
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
84
86
|
} catch (err) {
|
|
@@ -106,15 +108,15 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
106
108
|
this.#stream = undefined;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
async send({
|
|
111
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
110
112
|
const message = formatOutboundSegments(payload);
|
|
111
|
-
const { action, params } = buildSendAction(
|
|
113
|
+
const { action, params } = buildSendAction(conversation, message);
|
|
112
114
|
const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
|
|
113
|
-
const messageId = formatOutboundMessageId(
|
|
114
|
-
logger.debug(formatCompact({
|
|
115
|
+
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
116
|
+
this.#logger.debug(formatCompact({
|
|
115
117
|
op: 'milky_send',
|
|
116
|
-
endpoint: this.#options.config.
|
|
117
|
-
target
|
|
118
|
+
endpoint: this.#options.config.id,
|
|
119
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
118
120
|
messageId,
|
|
119
121
|
mode: 'sse',
|
|
120
122
|
}));
|
|
@@ -218,33 +220,32 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
221
|
-
const
|
|
223
|
+
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
224
|
+
const target = `${conversation.kind}:${conversation.id}`;
|
|
222
225
|
const content = formatInboundContent(data);
|
|
223
226
|
const segments = formatInboundSegments(data);
|
|
224
227
|
const audioUrl = extractInboundAudioUrl(data);
|
|
225
228
|
const nickname = senderNickname(data);
|
|
226
229
|
const mentioned = isMentioned(data, event.self_id);
|
|
227
230
|
void this.#options.gateway.receive({
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
conversation,
|
|
232
|
+
message: { conversation, id: formatInboundMessageId(data) },
|
|
230
233
|
content,
|
|
231
234
|
segments,
|
|
232
|
-
sender: String(data.sender_id),
|
|
233
|
-
|
|
235
|
+
sender: { id: String(data.sender_id), name: nickname },
|
|
236
|
+
endpointId: this.#options.config.id,
|
|
237
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
234
238
|
metadata: Object.freeze({
|
|
235
239
|
message_scene: data.message_scene,
|
|
236
240
|
peer_id: String(data.peer_id),
|
|
237
241
|
sender_id: String(data.sender_id),
|
|
238
242
|
message_seq: data.message_seq,
|
|
239
|
-
endpoint: this.#options.config.name,
|
|
240
243
|
time: data.time ?? event.time,
|
|
241
244
|
self_id: event.self_id != null ? String(event.self_id) : undefined,
|
|
242
245
|
...(nickname ? { nickname } : {}),
|
|
243
|
-
...(mentioned ? { mentioned: true } : {}),
|
|
244
|
-
...(audioUrl ? { audio_url: audioUrl } : {}),
|
|
245
246
|
}),
|
|
246
247
|
}).catch((err) => {
|
|
247
|
-
logger.warn(formatCompact({
|
|
248
|
+
this.#logger.warn(formatCompact({
|
|
248
249
|
op: 'milky_gateway_receive_failed',
|
|
249
250
|
target,
|
|
250
251
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -264,8 +265,8 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
264
265
|
onOpen: () => {
|
|
265
266
|
if (settled) return;
|
|
266
267
|
settled = true;
|
|
267
|
-
logger.debug(formatCompact({
|
|
268
|
-
endpoint: this.#options.config.
|
|
268
|
+
this.#logger.debug(formatCompact({
|
|
269
|
+
endpoint: this.#options.config.id,
|
|
269
270
|
mode: 'sse',
|
|
270
271
|
url: safeUrl,
|
|
271
272
|
}));
|
|
@@ -273,9 +274,9 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
273
274
|
},
|
|
274
275
|
onMessage: (data) => this.#onMessage(data),
|
|
275
276
|
onError: (error) => {
|
|
276
|
-
logger.warn(formatCompact({
|
|
277
|
+
this.#logger.warn(formatCompact({
|
|
277
278
|
op: 'sse_error',
|
|
278
|
-
endpoint: this.#options.config.
|
|
279
|
+
endpoint: this.#options.config.id,
|
|
279
280
|
ok: false,
|
|
280
281
|
error: error.message,
|
|
281
282
|
}));
|
|
@@ -296,9 +297,8 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
296
297
|
void stream.closed.then(() => {
|
|
297
298
|
// stop-during-connect 竞态由基座静默 settle(主动停止不算失败),此处仅对运行期断开告警
|
|
298
299
|
if (this.#lifecycle.state !== 'stopped') {
|
|
299
|
-
logger.warn(formatCompact({
|
|
300
|
-
|
|
301
|
-
endpoint: this.#options.config.name,
|
|
300
|
+
this.#logger.warn(formatCompact({
|
|
301
|
+
op: 'disconnect',
|
|
302
302
|
mode: 'sse',
|
|
303
303
|
reconnect_ms: this.#options.config.reconnect_interval,
|
|
304
304
|
}));
|
|
@@ -318,9 +318,9 @@ export class MilkySseEndpoint implements EndpointInstance {
|
|
|
318
318
|
const event = JSON.parse(data) as MilkyEvent;
|
|
319
319
|
this.admit(event);
|
|
320
320
|
} catch (error) {
|
|
321
|
-
logger.warn(formatCompact({
|
|
321
|
+
this.#logger.warn(formatCompact({
|
|
322
322
|
op: 'milky_parse_failed',
|
|
323
|
-
endpoint: this.#options.config.
|
|
323
|
+
endpoint: this.#options.config.id,
|
|
324
324
|
mode: 'sse',
|
|
325
325
|
error: error instanceof Error ? error.message : String(error),
|
|
326
326
|
}));
|
package/src/webhook-endpoint.ts
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
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 {
|
|
5
|
+
import type {
|
|
6
|
+
EndpointInstance,
|
|
7
|
+
EndpointManagement,
|
|
8
|
+
EndpointSendRequest,
|
|
9
|
+
} from '@zhin.js/adapter';
|
|
6
10
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
11
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
|
-
import { formatCompact,
|
|
12
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
9
13
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
14
|
import { readRequestBody, verifyMilkyAccessToken } from './milky-auth.js';
|
|
11
15
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
@@ -17,10 +21,10 @@ import {
|
|
|
17
21
|
formatInboundContent,
|
|
18
22
|
formatInboundMessageId,
|
|
19
23
|
formatInboundSegments,
|
|
20
|
-
formatInboundTarget,
|
|
21
24
|
formatOutboundMessageId,
|
|
22
25
|
formatOutboundSegments,
|
|
23
26
|
isMentioned,
|
|
27
|
+
milkyInboundConversation,
|
|
24
28
|
parseMessageReceiveData,
|
|
25
29
|
parseMilkyMessageId,
|
|
26
30
|
senderNickname,
|
|
@@ -29,8 +33,6 @@ import {
|
|
|
29
33
|
type MilkyWebhookConfig,
|
|
30
34
|
} from './protocol.js';
|
|
31
35
|
|
|
32
|
-
const logger = getLogger('milky');
|
|
33
|
-
|
|
34
36
|
export interface MilkyWebhookEndpointOptions {
|
|
35
37
|
readonly id: CapabilityId;
|
|
36
38
|
readonly gateway: MessageGateway;
|
|
@@ -40,6 +42,8 @@ export interface MilkyWebhookEndpointOptions {
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
45
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
46
|
+
|
|
43
47
|
readonly #options: MilkyWebhookEndpointOptions;
|
|
44
48
|
readonly #callApi: typeof callApi;
|
|
45
49
|
readonly management: EndpointManagement = createMilkyEndpointManagement(this);
|
|
@@ -49,6 +53,7 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
|
49
53
|
#unregisterAgent?: () => void;
|
|
50
54
|
|
|
51
55
|
constructor(options: MilkyWebhookEndpointOptions) {
|
|
56
|
+
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
52
57
|
this.#options = options;
|
|
53
58
|
this.#callApi = options.callApi ?? callApi;
|
|
54
59
|
}
|
|
@@ -56,11 +61,11 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
|
56
61
|
async start(): Promise<void> {
|
|
57
62
|
if (this.#started) return;
|
|
58
63
|
this.#started = true;
|
|
59
|
-
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.
|
|
64
|
+
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
|
|
60
65
|
this.#setupRoutes();
|
|
61
|
-
logger.info(formatCompact({
|
|
66
|
+
this.#logger.info(formatCompact({
|
|
62
67
|
op: 'listen',
|
|
63
|
-
endpoint: this.#options.config.
|
|
68
|
+
endpoint: this.#options.config.id,
|
|
64
69
|
mode: 'webhook',
|
|
65
70
|
path: this.#options.config.path,
|
|
66
71
|
}));
|
|
@@ -80,18 +85,18 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
|
80
85
|
this.#unregisterAgent?.();
|
|
81
86
|
this.#unregisterAgent = undefined;
|
|
82
87
|
this.#started = false;
|
|
83
|
-
logger.debug(formatCompact({ op: 'disconnect'
|
|
88
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
84
89
|
}
|
|
85
90
|
|
|
86
|
-
async send({
|
|
91
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
87
92
|
const message = formatOutboundSegments(payload);
|
|
88
|
-
const { action, params } = buildSendAction(
|
|
93
|
+
const { action, params } = buildSendAction(conversation, message);
|
|
89
94
|
const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
|
|
90
|
-
const messageId = formatOutboundMessageId(
|
|
91
|
-
logger.debug(formatCompact({
|
|
95
|
+
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
96
|
+
this.#logger.debug(formatCompact({
|
|
92
97
|
op: 'milky_send',
|
|
93
|
-
endpoint: this.#options.config.
|
|
94
|
-
target
|
|
98
|
+
endpoint: this.#options.config.id,
|
|
99
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
95
100
|
messageId,
|
|
96
101
|
}));
|
|
97
102
|
return messageId;
|
|
@@ -194,33 +199,32 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
197
|
-
const
|
|
202
|
+
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
203
|
+
const target = `${conversation.kind}:${conversation.id}`;
|
|
198
204
|
const content = formatInboundContent(data);
|
|
199
205
|
const segments = formatInboundSegments(data);
|
|
200
206
|
const audioUrl = extractInboundAudioUrl(data);
|
|
201
207
|
const nickname = senderNickname(data);
|
|
202
208
|
const mentioned = isMentioned(data, event.self_id);
|
|
203
209
|
void this.#options.gateway.receive({
|
|
204
|
-
|
|
205
|
-
|
|
210
|
+
conversation,
|
|
211
|
+
message: { conversation, id: formatInboundMessageId(data) },
|
|
206
212
|
content,
|
|
207
213
|
segments,
|
|
208
|
-
sender: String(data.sender_id),
|
|
209
|
-
|
|
214
|
+
sender: { id: String(data.sender_id), name: nickname },
|
|
215
|
+
endpointId: this.#options.config.id,
|
|
216
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
210
217
|
metadata: Object.freeze({
|
|
211
218
|
message_scene: data.message_scene,
|
|
212
219
|
peer_id: String(data.peer_id),
|
|
213
220
|
sender_id: String(data.sender_id),
|
|
214
221
|
message_seq: data.message_seq,
|
|
215
|
-
endpoint: this.#options.config.name,
|
|
216
222
|
time: data.time ?? event.time,
|
|
217
223
|
self_id: event.self_id != null ? String(event.self_id) : undefined,
|
|
218
224
|
...(nickname ? { nickname } : {}),
|
|
219
|
-
...(mentioned ? { mentioned: true } : {}),
|
|
220
|
-
...(audioUrl ? { audio_url: audioUrl } : {}),
|
|
221
225
|
}),
|
|
222
226
|
}).catch((err) => {
|
|
223
|
-
logger.warn(formatCompact({
|
|
227
|
+
this.#logger.warn(formatCompact({
|
|
224
228
|
op: 'milky_gateway_receive_failed',
|
|
225
229
|
target,
|
|
226
230
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -257,7 +261,7 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
|
|
|
257
261
|
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
258
262
|
response.end(JSON.stringify({ status: 'ok' }));
|
|
259
263
|
} catch (error) {
|
|
260
|
-
logger.error('Milky webhook error:', error);
|
|
264
|
+
this.#logger.error('Milky webhook error:', error);
|
|
261
265
|
if (!response.headersSent) {
|
|
262
266
|
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
263
267
|
response.end(JSON.stringify({ message: 'Internal Server Error' }));
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
type EndpointInstance,
|
|
9
9
|
type EndpointLifecycle,
|
|
10
10
|
type EndpointManagement,
|
|
11
|
+
type EndpointSendRequest,
|
|
11
12
|
} from '@zhin.js/adapter';
|
|
12
13
|
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
13
|
-
import { formatCompact,
|
|
14
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
14
15
|
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
15
16
|
import { createMilkyEndpointManagement } from './endpoint-management.js';
|
|
16
17
|
import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
|
|
@@ -22,10 +23,10 @@ import {
|
|
|
22
23
|
formatInboundContent,
|
|
23
24
|
formatInboundMessageId,
|
|
24
25
|
formatInboundSegments,
|
|
25
|
-
formatInboundTarget,
|
|
26
26
|
formatOutboundMessageId,
|
|
27
27
|
formatOutboundSegments,
|
|
28
28
|
isMentioned,
|
|
29
|
+
milkyInboundConversation,
|
|
29
30
|
parseMessageReceiveData,
|
|
30
31
|
parseMilkyMessageId,
|
|
31
32
|
senderNickname,
|
|
@@ -35,7 +36,6 @@ import {
|
|
|
35
36
|
} from './protocol.js';
|
|
36
37
|
import type { MilkyWsCreateOptions, MilkyWsSocket } from './ws-types.js';
|
|
37
38
|
|
|
38
|
-
const logger = getLogger('milky');
|
|
39
39
|
const WS_OPEN = 1;
|
|
40
40
|
|
|
41
41
|
export interface MilkyWsEndpointOptions {
|
|
@@ -50,6 +50,8 @@ export interface MilkyWsEndpointOptions {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export class MilkyWsEndpoint implements EndpointInstance {
|
|
53
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
54
|
+
|
|
53
55
|
readonly #options: MilkyWsEndpointOptions;
|
|
54
56
|
readonly #callApi: typeof callApi;
|
|
55
57
|
readonly management: EndpointManagement = createMilkyEndpointManagement(this);
|
|
@@ -59,10 +61,11 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
59
61
|
#unregisterAgent?: () => void;
|
|
60
62
|
|
|
61
63
|
constructor(options: MilkyWsEndpointOptions) {
|
|
64
|
+
this.#logger = getAdapterLogger('milky', options.config.id);
|
|
62
65
|
this.#options = options;
|
|
63
66
|
this.#callApi = options.callApi ?? callApi;
|
|
64
67
|
this.#lifecycle = createEndpointLifecycle({
|
|
65
|
-
name: options.config.
|
|
68
|
+
name: options.config.id,
|
|
66
69
|
// reconnect_interval 旧语义为固定间隔:multiplier 1 + 无 jitter + 不封顶
|
|
67
70
|
reconnect: {
|
|
68
71
|
initialIntervalMs: options.config.reconnect_interval,
|
|
@@ -75,7 +78,7 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
75
78
|
|
|
76
79
|
async start(): Promise<void> {
|
|
77
80
|
if (this.#lifecycle.started) return;
|
|
78
|
-
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.
|
|
81
|
+
this.#unregisterAgent = registerMilkyAgentEndpoint(this.#options.config.id, this);
|
|
79
82
|
try {
|
|
80
83
|
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
81
84
|
} catch (err) {
|
|
@@ -109,15 +112,15 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
async send({
|
|
115
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
113
116
|
const message = formatOutboundSegments(payload);
|
|
114
|
-
const { action, params } = buildSendAction(
|
|
117
|
+
const { action, params } = buildSendAction(conversation, message);
|
|
115
118
|
const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
|
|
116
|
-
const messageId = formatOutboundMessageId(
|
|
117
|
-
logger.debug(formatCompact({
|
|
119
|
+
const messageId = formatOutboundMessageId(conversation, data?.message_seq);
|
|
120
|
+
this.#logger.debug(formatCompact({
|
|
118
121
|
op: 'milky_send',
|
|
119
|
-
endpoint: this.#options.config.
|
|
120
|
-
target
|
|
122
|
+
endpoint: this.#options.config.id,
|
|
123
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
121
124
|
messageId,
|
|
122
125
|
}));
|
|
123
126
|
return messageId;
|
|
@@ -222,33 +225,32 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
#admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
|
|
225
|
-
const
|
|
228
|
+
const conversation = milkyInboundConversation(String(this.#options.id), data);
|
|
229
|
+
const target = `${conversation.kind}:${conversation.id}`;
|
|
226
230
|
const content = formatInboundContent(data);
|
|
227
231
|
const segments = formatInboundSegments(data);
|
|
228
232
|
const audioUrl = extractInboundAudioUrl(data);
|
|
229
233
|
const nickname = senderNickname(data);
|
|
230
234
|
const mentioned = isMentioned(data, event.self_id);
|
|
231
235
|
void this.#options.gateway.receive({
|
|
232
|
-
|
|
233
|
-
|
|
236
|
+
conversation,
|
|
237
|
+
message: { conversation, id: formatInboundMessageId(data) },
|
|
234
238
|
content,
|
|
235
239
|
segments,
|
|
236
|
-
sender: String(data.sender_id),
|
|
237
|
-
|
|
240
|
+
sender: { id: String(data.sender_id), name: nickname },
|
|
241
|
+
endpointId: this.#options.config.id,
|
|
242
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
238
243
|
metadata: Object.freeze({
|
|
239
244
|
message_scene: data.message_scene,
|
|
240
245
|
peer_id: String(data.peer_id),
|
|
241
246
|
sender_id: String(data.sender_id),
|
|
242
247
|
message_seq: data.message_seq,
|
|
243
|
-
endpoint: this.#options.config.name,
|
|
244
248
|
time: data.time ?? event.time,
|
|
245
249
|
self_id: event.self_id != null ? String(event.self_id) : undefined,
|
|
246
250
|
...(nickname ? { nickname } : {}),
|
|
247
|
-
...(mentioned ? { mentioned: true } : {}),
|
|
248
|
-
...(audioUrl ? { audio_url: audioUrl } : {}),
|
|
249
251
|
}),
|
|
250
252
|
}).catch((err) => {
|
|
251
|
-
logger.warn(formatCompact({
|
|
253
|
+
this.#logger.warn(formatCompact({
|
|
252
254
|
op: 'milky_gateway_receive_failed',
|
|
253
255
|
target,
|
|
254
256
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -278,14 +280,14 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
278
280
|
if (settled) return;
|
|
279
281
|
settled = true;
|
|
280
282
|
if (!this.#options.config.access_token) {
|
|
281
|
-
logger.warn(formatCompact({
|
|
282
|
-
endpoint: this.#options.config.
|
|
283
|
+
this.#logger.warn(formatCompact({
|
|
284
|
+
endpoint: this.#options.config.id,
|
|
283
285
|
ok: false,
|
|
284
286
|
error: 'missing access_token',
|
|
285
287
|
}));
|
|
286
288
|
}
|
|
287
|
-
logger.debug(formatCompact({
|
|
288
|
-
endpoint: this.#options.config.
|
|
289
|
+
this.#logger.debug(formatCompact({
|
|
290
|
+
endpoint: this.#options.config.id,
|
|
289
291
|
mode: 'ws',
|
|
290
292
|
url: safeUrl,
|
|
291
293
|
}));
|
|
@@ -318,9 +320,8 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
318
320
|
: codeNum === 1006
|
|
319
321
|
? ' [异常关闭]'
|
|
320
322
|
: '';
|
|
321
|
-
logger.warn(formatCompact({
|
|
323
|
+
this.#logger.warn(formatCompact({
|
|
322
324
|
op: 'disconnect',
|
|
323
|
-
endpoint: this.#options.config.name,
|
|
324
325
|
code: codeNum,
|
|
325
326
|
error: `${reasonStr || 'closed'}${codeHint}`,
|
|
326
327
|
reconnect_ms: this.#options.config.reconnect_interval,
|
|
@@ -335,9 +336,9 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
335
336
|
|
|
336
337
|
ws.on('error', (err) => {
|
|
337
338
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
338
|
-
logger.warn(formatCompact({
|
|
339
|
+
this.#logger.warn(formatCompact({
|
|
339
340
|
op: 'ws_error',
|
|
340
|
-
endpoint: this.#options.config.
|
|
341
|
+
endpoint: this.#options.config.id,
|
|
341
342
|
ok: false,
|
|
342
343
|
error: error.message,
|
|
343
344
|
}));
|
|
@@ -361,9 +362,9 @@ export class MilkyWsEndpoint implements EndpointInstance {
|
|
|
361
362
|
const event = JSON.parse(raw) as MilkyEvent;
|
|
362
363
|
this.admit(event);
|
|
363
364
|
} catch (error) {
|
|
364
|
-
logger.warn(formatCompact({
|
|
365
|
+
this.#logger.warn(formatCompact({
|
|
365
366
|
op: 'milky_parse_failed',
|
|
366
|
-
endpoint: this.#options.config.
|
|
367
|
+
endpoint: this.#options.config.id,
|
|
367
368
|
error: error instanceof Error ? error.message : String(error),
|
|
368
369
|
}));
|
|
369
370
|
}
|