@zhin.js/adapter-milky 6.0.2 → 6.0.3

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # @zhin.js/adapter-milky
2
2
 
3
+ ## 6.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [afc0e66]
8
+ - Updated dependencies [2e41ad5]
9
+ - Updated dependencies [9f57124]
10
+ - @zhin.js/core@1.5.2
11
+ - @zhin.js/adapter@1.1.5
12
+ - @zhin.js/im-contract@1.0.1
13
+ - @zhin.js/plugin-runtime@1.1.3
14
+ - @zhin.js/command@1.0.7
15
+ - @zhin.js/host-http@1.0.6
16
+ - @zhin.js/agent@1.1.3
17
+ - zhin.js@6.0.2
18
+
3
19
  ## 6.0.2
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -8,7 +8,7 @@ Zhin.js [Milky](https://milky.ntqqrev.org/) 协议适配器(Plugin Runtime)
8
8
  - 约定式 `defineAdapter` / `definePlugin`(无需 `usePlugin`)
9
9
  - **正向 WebSocket**(`connection: ws`):应用连协议端 `ws(s)://baseUrl/event`
10
10
  - `access_token` 鉴权(Bearer + query)
11
- - 入站经 `messageGatewayToken`;出站 `send({ target, payload })` → HTTP `send_*_message`
11
+ - 入站经 `messageGatewayToken`;出站 `send({ conversation, payload })` → HTTP `send_*_message`
12
12
 
13
13
  ## 安装
14
14
 
@@ -23,8 +23,8 @@ pnpm add @zhin.js/adapter-milky
23
23
  - `@zhin.js/plugin-runtime` — `plugin.ts`(`definePlugin`)
24
24
  - 配置经插件 `schema.json` 落到 `plugins.<instanceKey>`
25
25
 
26
- 入站:`gateway.receive({ adapter, target: "private:uid"|"group:gid", content, sender, metadata })`
27
- 出站:`send({ target, payload })` → HTTP `send_private_message` / `send_group_message`(payload 已由 gateway/core 渲染;无 segment-mapper)
26
+ 入站:`gateway.receive({ conversation: ConversationRef(kind: "private"|"group", id), message, content, sender, metadata })`
27
+ 出站:`send({ conversation, payload })` → HTTP `send_private_message` / `send_group_message`(payload 已由 gateway/core 渲染;无 segment-mapper)
28
28
 
29
29
  ## 最小配置
30
30
 
package/lib/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { buildSendAction, buildSseConnectOptions, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, isMessageReceiveEvent, parseMessageReceiveData, parseMilkyMessageId, parseSendTarget, resolveMilkyConfig, senderNickname, type MilkyAdapterConfig, type MilkyApiClientOptions, type MilkyApiResponse, type MilkyConfigBase, type MilkyEndpointConfig, type MilkyEvent, type MilkyIncomingMessage, type MilkyIncomingSegment, type MilkyOutgoingSegment, type MilkySseConfig, type MilkyWebhookConfig, type MilkyWireSegment, type MilkyWsConfig, type MilkyWssConfig, type ParsedSendTarget, type ResolvedMilkyConfig, } from './protocol.js';
1
+ export { buildSendAction, buildSseConnectOptions, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatOutboundSegments, isMentioned, isMessageReceiveEvent, milkyInboundConversation, parseMessageReceiveData, resolveMilkyConfig, senderNickname, type MilkyAdapterConfig, type MilkyApiClientOptions, type MilkyApiResponse, type MilkyConfigBase, type MilkyEndpointConfig, type MilkyEvent, type MilkyIncomingMessage, type MilkyIncomingSegment, type MilkyOutgoingSegment, type MilkySseConfig, type MilkyWebhookConfig, type MilkyWireSegment, type MilkyWsConfig, type MilkyWssConfig, type ResolvedMilkyConfig, } from './protocol.js';
2
2
  export { MilkySseEndpoint, MilkyWebhookEndpoint, MilkyWssEndpoint, MilkyWsEndpoint, consumeSseBuffer, openSseStream, readRequestBody, verifyMilkyAccessToken, type CreateMilkySseStream, type MilkySseEndpointOptions, type MilkyWebhookEndpointOptions, type MilkyWssEndpointOptions, type MilkyWsCreateOptions, type MilkyWsEndpointOptions, type MilkyWsSocket, } from './endpoint.js';
3
3
  export { getMilkyAgentDeps, registerMilkyAgentEndpoint, setMilkyAgentDeps, type MilkyAgentDeps, type MilkyAgentEndpoint, } from './milky-agent-deps.js';
package/lib/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { buildSendAction, buildSseConnectOptions, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, isMessageReceiveEvent, parseMessageReceiveData, parseMilkyMessageId, parseSendTarget, resolveMilkyConfig, senderNickname, } from './protocol.js';
1
+ export { buildSendAction, buildSseConnectOptions, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatOutboundSegments, isMentioned, isMessageReceiveEvent, milkyInboundConversation, parseMessageReceiveData, resolveMilkyConfig, senderNickname, } from './protocol.js';
2
2
  export { MilkySseEndpoint, MilkyWebhookEndpoint, MilkyWssEndpoint, MilkyWsEndpoint, consumeSseBuffer, openSseStream, readRequestBody, verifyMilkyAccessToken, } from './endpoint.js';
3
3
  export { getMilkyAgentDeps, registerMilkyAgentEndpoint, setMilkyAgentDeps, } from './milky-agent-deps.js';
package/lib/protocol.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Spec: https://milky.ntqqrev.org/
5
5
  */
6
6
  import type { Segment } from '@zhin.js/core/runtime';
7
+ import type { ConversationRef } from '@zhin.js/im-contract';
7
8
  /** Transitional legacy endpoint row (`endpoints[]` with `context: milky`). */
8
9
  export interface MilkyLegacyEndpointRow {
9
10
  readonly context?: string;
@@ -103,10 +104,6 @@ export interface MilkyOutgoingSegment {
103
104
  type: string;
104
105
  data: Record<string, unknown>;
105
106
  }
106
- export interface ParsedSendTarget {
107
- readonly message_type: 'private' | 'group';
108
- readonly id: string;
109
- }
110
107
  export interface MilkyApiClientOptions {
111
108
  readonly baseUrl: string;
112
109
  readonly access_token?: string;
@@ -122,8 +119,11 @@ export declare function parseMessageReceiveData(event: MilkyEvent): MilkyIncomin
122
119
  export declare function isMessageReceiveEvent(event: MilkyEvent): event is MilkyEvent & {
123
120
  data: MilkyIncomingMessage;
124
121
  };
125
- /** Gateway reply target:`private:uid` / `group:gid` */
126
- export declare function formatInboundTarget(data: MilkyIncomingMessage): string;
122
+ /**
123
+ * 入站归一化 ConversationRef:`friend`(私聊)→ `private`;`group` → `group`;
124
+ * `temp`(群临时会话)→ 群容器内的 `private` 会话(parent = 来源群)。
125
+ */
126
+ export declare function milkyInboundConversation(endpointId: string, data: MilkyIncomingMessage): ConversationRef;
127
127
  export declare function formatInboundContent(data: MilkyIncomingMessage): string;
128
128
  /**
129
129
  * 入站消息段 → canonical Segment[](与 formatInboundContent 纯文本视图同源双轨)。
@@ -139,7 +139,6 @@ export declare function senderNickname(data: MilkyIncomingMessage): string | und
139
139
  /** 消息段中含 @ 本机(mention 段 user_id 等于协议端上报的 self_id)。 */
140
140
  export declare function isMentioned(data: MilkyIncomingMessage, selfId: number | undefined): boolean;
141
141
  export declare function formatInboundMessageId(data: MilkyIncomingMessage): string;
142
- export declare function parseSendTarget(target: string): ParsedSendTarget;
143
142
  /**
144
143
  * Wire-encode an already-rendered outbound payload into Milky outgoing segments.
145
144
  * Segment canonicalization is intentionally not done here.
@@ -147,12 +146,15 @@ export declare function parseSendTarget(target: string): ParsedSendTarget;
147
146
  * file 段 Milky 消息 API 无对应 wire 段(文件走 upload_* API),warn + 丢弃。
148
147
  */
149
148
  export declare function formatOutboundSegments(payload: unknown): MilkyOutgoingSegment[];
150
- export declare function buildSendAction(target: string, message: MilkyOutgoingSegment[]): {
149
+ export declare function buildSendAction(conversation: ConversationRef, message: MilkyOutgoingSegment[]): {
151
150
  action: string;
152
151
  params: Record<string, unknown>;
153
152
  };
154
- /** message_seq result → gateway message id */
155
- export declare function formatOutboundMessageId(target: string, messageSeq: number | undefined): string;
153
+ /**
154
+ * message_seq result gateway message id(`scene:peer:seq` 复合格式,
155
+ * 与 formatInboundMessageId 同源,仅供 recall 链在本端点边界内解析)。
156
+ */
157
+ export declare function formatOutboundMessageId(conversation: ConversationRef, messageSeq: number | undefined): string;
156
158
  export declare function parseMilkyMessageId(msgId: string): {
157
159
  message_scene: 'friend' | 'group' | 'temp';
158
160
  peer_id: number;
package/lib/protocol.js CHANGED
@@ -125,10 +125,19 @@ export function parseMessageReceiveData(event) {
125
125
  export function isMessageReceiveEvent(event) {
126
126
  return parseMessageReceiveData(event) != null;
127
127
  }
128
- /** Gateway reply target:`private:uid` / `group:gid` */
129
- export function formatInboundTarget(data) {
130
- const isGroup = data.message_scene === 'group';
131
- return `${isGroup ? 'group' : 'private'}:${data.peer_id}`;
128
+ /**
129
+ * 入站归一化 → ConversationRef:`friend`(私聊)→ `private`;`group` → `group`;
130
+ * `temp`(群临时会话)→ 群容器内的 `private` 会话(parent = 来源群)。
131
+ */
132
+ export function milkyInboundConversation(endpointId, data) {
133
+ return {
134
+ endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
135
+ kind: data.message_scene === 'group' ? 'group' : 'private',
136
+ id: String(data.peer_id),
137
+ ...(data.message_scene === 'temp' && data.group
138
+ ? { parent: { kind: 'group', id: String(data.group.group_id) } }
139
+ : {}),
140
+ };
132
141
  }
133
142
  export function formatInboundContent(data) {
134
143
  return data.segments.map((seg) => {
@@ -290,19 +299,6 @@ export function isMentioned(data, selfId) {
290
299
  export function formatInboundMessageId(data) {
291
300
  return `${data.message_scene}:${data.peer_id}:${data.message_seq}`;
292
301
  }
293
- export function parseSendTarget(target) {
294
- const sep = target.indexOf(':');
295
- if (sep <= 0) {
296
- return { message_type: 'private', id: target };
297
- }
298
- const head = target.slice(0, sep);
299
- const rest = target.slice(sep + 1);
300
- if (head === 'group')
301
- return { message_type: 'group', id: rest };
302
- if (head === 'private')
303
- return { message_type: 'private', id: rest };
304
- return { message_type: 'private', id: target };
305
- }
306
302
  /**
307
303
  * 解析 image/audio/video 段的投递 uri(canonical MediaRef 唯一来源):
308
304
  * - kind=url → http(s):// 直发;
@@ -432,13 +428,12 @@ export function formatOutboundSegments(payload) {
432
428
  }
433
429
  return out.length ? out : [{ type: 'text', data: { text: '' } }];
434
430
  }
435
- export function buildSendAction(target, message) {
436
- const parsed = parseSendTarget(target);
437
- if (parsed.message_type === 'group') {
431
+ export function buildSendAction(conversation, message) {
432
+ if (conversation.kind === 'group') {
438
433
  return {
439
434
  action: 'send_group_message',
440
435
  params: {
441
- group_id: parseInt(parsed.id, 10),
436
+ group_id: parseInt(conversation.id, 10),
442
437
  message,
443
438
  },
444
439
  };
@@ -446,18 +441,20 @@ export function buildSendAction(target, message) {
446
441
  return {
447
442
  action: 'send_private_message',
448
443
  params: {
449
- user_id: parseInt(parsed.id, 10),
444
+ user_id: parseInt(conversation.id, 10),
450
445
  message,
451
446
  },
452
447
  };
453
448
  }
454
- /** message_seq result → gateway message id */
455
- export function formatOutboundMessageId(target, messageSeq) {
449
+ /**
450
+ * message_seq result → gateway message id(`scene:peer:seq` 复合格式,
451
+ * 与 formatInboundMessageId 同源,仅供 recall 链在本端点边界内解析)。
452
+ */
453
+ export function formatOutboundMessageId(conversation, messageSeq) {
456
454
  if (messageSeq == null)
457
455
  return '';
458
- const parsed = parseSendTarget(target);
459
- const scene = parsed.message_type === 'group' ? 'group' : 'friend';
460
- return `${scene}:${parsed.id}:${messageSeq}`;
456
+ const scene = conversation.kind === 'group' ? 'group' : 'friend';
457
+ return `${scene}:${conversation.id}:${messageSeq}`;
461
458
  }
462
459
  export function parseMilkyMessageId(msgId) {
463
460
  const parts = msgId.split(':');
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Milky SSE client endpoint — GET text/event-stream on /event.
3
3
  */
4
- import { type EndpointInstance, type EndpointManagement } from '@zhin.js/adapter';
4
+ import { type EndpointInstance, type EndpointManagement, type EndpointSendRequest } from '@zhin.js/adapter';
5
5
  import type { MessageGateway } from '@zhin.js/core/runtime';
6
6
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
7
7
  import { callApi, type MilkyEvent, type MilkySseConfig } from './protocol.js';
@@ -28,10 +28,7 @@ export declare class MilkySseEndpoint implements EndpointInstance {
28
28
  open(): void;
29
29
  close(): void;
30
30
  stop(): Promise<void>;
31
- send({ target, payload }: {
32
- readonly target: string;
33
- readonly payload: unknown;
34
- }): Promise<string>;
31
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
35
32
  callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
36
33
  recallMessage(id: string): Promise<void>;
37
34
  kickMember(groupId: number, userId: number, rejectAddRequest?: boolean): Promise<boolean>;
@@ -5,7 +5,7 @@ import { createEndpointLifecycle, } from '@zhin.js/adapter';
5
5
  import { formatCompact, getLogger } from '@zhin.js/logger';
6
6
  import { createMilkyEndpointManagement } from './endpoint-management.js';
7
7
  import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
8
- import { buildSendAction, buildSseConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
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
10
  const logger = getLogger('milky');
11
11
  export class MilkySseEndpoint {
@@ -58,15 +58,15 @@ export class MilkySseEndpoint {
58
58
  this.#stream?.close();
59
59
  this.#stream = undefined;
60
60
  }
61
- async send({ target, payload }) {
61
+ async send({ conversation, payload }) {
62
62
  const message = formatOutboundSegments(payload);
63
- const { action, params } = buildSendAction(target, message);
63
+ const { action, params } = buildSendAction(conversation, message);
64
64
  const data = await this.callApi(action, params);
65
- const messageId = formatOutboundMessageId(target, data?.message_seq);
65
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
66
66
  logger.debug(formatCompact({
67
67
  op: 'milky_send',
68
68
  endpoint: this.#options.config.name,
69
- target,
69
+ target: `${conversation.kind}:${conversation.id}`,
70
70
  messageId,
71
71
  mode: 'sse',
72
72
  }));
@@ -159,19 +159,19 @@ export class MilkySseEndpoint {
159
159
  };
160
160
  }
161
161
  #admitMessage(data, event) {
162
- const target = formatInboundTarget(data);
162
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
163
+ const target = `${conversation.kind}:${conversation.id}`;
163
164
  const content = formatInboundContent(data);
164
165
  const segments = formatInboundSegments(data);
165
166
  const audioUrl = extractInboundAudioUrl(data);
166
167
  const nickname = senderNickname(data);
167
168
  const mentioned = isMentioned(data, event.self_id);
168
169
  void this.#options.gateway.receive({
169
- adapter: this.#options.id,
170
- target,
170
+ conversation,
171
+ message: { conversation, id: formatInboundMessageId(data) },
171
172
  content,
172
173
  segments,
173
174
  sender: String(data.sender_id),
174
- id: formatInboundMessageId(data),
175
175
  metadata: Object.freeze({
176
176
  message_scene: data.message_scene,
177
177
  peer_id: String(data.peer_id),
@@ -1,4 +1,4 @@
1
- import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
1
+ import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
2
2
  import type { MessageGateway } from '@zhin.js/core/runtime';
3
3
  import type { HttpHost } from '@zhin.js/host-http';
4
4
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
@@ -18,10 +18,7 @@ export declare class MilkyWebhookEndpoint implements EndpointInstance {
18
18
  open(): void;
19
19
  close(): void;
20
20
  stop(): Promise<void>;
21
- send({ target, payload }: {
22
- readonly target: string;
23
- readonly payload: unknown;
24
- }): Promise<string>;
21
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
25
22
  callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
26
23
  recallMessage(id: string): Promise<void>;
27
24
  kickMember(groupId: number, userId: number, rejectAddRequest?: boolean): Promise<boolean>;
@@ -2,7 +2,7 @@ import { formatCompact, getLogger } from '@zhin.js/logger';
2
2
  import { readRequestBody, verifyMilkyAccessToken } from './milky-auth.js';
3
3
  import { createMilkyEndpointManagement } from './endpoint-management.js';
4
4
  import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
5
- import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
5
+ import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
6
6
  const logger = getLogger('milky');
7
7
  export class MilkyWebhookEndpoint {
8
8
  #options;
@@ -44,15 +44,15 @@ export class MilkyWebhookEndpoint {
44
44
  this.#started = false;
45
45
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
46
46
  }
47
- async send({ target, payload }) {
47
+ async send({ conversation, payload }) {
48
48
  const message = formatOutboundSegments(payload);
49
- const { action, params } = buildSendAction(target, message);
49
+ const { action, params } = buildSendAction(conversation, message);
50
50
  const data = await this.callApi(action, params);
51
- const messageId = formatOutboundMessageId(target, data?.message_seq);
51
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
52
52
  logger.debug(formatCompact({
53
53
  op: 'milky_send',
54
54
  endpoint: this.#options.config.name,
55
- target,
55
+ target: `${conversation.kind}:${conversation.id}`,
56
56
  messageId,
57
57
  }));
58
58
  return messageId;
@@ -144,19 +144,19 @@ export class MilkyWebhookEndpoint {
144
144
  };
145
145
  }
146
146
  #admitMessage(data, event) {
147
- const target = formatInboundTarget(data);
147
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
148
+ const target = `${conversation.kind}:${conversation.id}`;
148
149
  const content = formatInboundContent(data);
149
150
  const segments = formatInboundSegments(data);
150
151
  const audioUrl = extractInboundAudioUrl(data);
151
152
  const nickname = senderNickname(data);
152
153
  const mentioned = isMentioned(data, event.self_id);
153
154
  void this.#options.gateway.receive({
154
- adapter: this.#options.id,
155
- target,
155
+ conversation,
156
+ message: { conversation, id: formatInboundMessageId(data) },
156
157
  content,
157
158
  segments,
158
159
  sender: String(data.sender_id),
159
- id: formatInboundMessageId(data),
160
160
  metadata: Object.freeze({
161
161
  message_scene: data.message_scene,
162
162
  peer_id: String(data.peer_id),
@@ -1,4 +1,4 @@
1
- import { type EndpointInstance, type EndpointManagement } from '@zhin.js/adapter';
1
+ import { type EndpointInstance, type EndpointManagement, type EndpointSendRequest } from '@zhin.js/adapter';
2
2
  import type { MessageGateway } from '@zhin.js/core/runtime';
3
3
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
4
4
  import { callApi, type MilkyEvent, type MilkyWsConfig } from './protocol.js';
@@ -18,10 +18,7 @@ export declare class MilkyWsEndpoint implements EndpointInstance {
18
18
  open(): void;
19
19
  close(): void;
20
20
  stop(): Promise<void>;
21
- send({ target, payload }: {
22
- readonly target: string;
23
- readonly payload: unknown;
24
- }): Promise<string>;
21
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
25
22
  /** Public API for agent tools / callers. */
26
23
  callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
27
24
  recallMessage(id: string): Promise<void>;
@@ -6,7 +6,7 @@ import { createEndpointLifecycle, } from '@zhin.js/adapter';
6
6
  import { formatCompact, getLogger } from '@zhin.js/logger';
7
7
  import { createMilkyEndpointManagement } from './endpoint-management.js';
8
8
  import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
9
- import { buildSendAction, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
9
+ import { buildSendAction, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
10
10
  const logger = getLogger('milky');
11
11
  const WS_OPEN = 1;
12
12
  export class MilkyWsEndpoint {
@@ -66,15 +66,15 @@ export class MilkyWsEndpoint {
66
66
  this.#ws = undefined;
67
67
  }
68
68
  }
69
- async send({ target, payload }) {
69
+ async send({ conversation, payload }) {
70
70
  const message = formatOutboundSegments(payload);
71
- const { action, params } = buildSendAction(target, message);
71
+ const { action, params } = buildSendAction(conversation, message);
72
72
  const data = await this.callApi(action, params);
73
- const messageId = formatOutboundMessageId(target, data?.message_seq);
73
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
74
74
  logger.debug(formatCompact({
75
75
  op: 'milky_send',
76
76
  endpoint: this.#options.config.name,
77
- target,
77
+ target: `${conversation.kind}:${conversation.id}`,
78
78
  messageId,
79
79
  }));
80
80
  return messageId;
@@ -168,19 +168,19 @@ export class MilkyWsEndpoint {
168
168
  };
169
169
  }
170
170
  #admitMessage(data, event) {
171
- const target = formatInboundTarget(data);
171
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
172
+ const target = `${conversation.kind}:${conversation.id}`;
172
173
  const content = formatInboundContent(data);
173
174
  const segments = formatInboundSegments(data);
174
175
  const audioUrl = extractInboundAudioUrl(data);
175
176
  const nickname = senderNickname(data);
176
177
  const mentioned = isMentioned(data, event.self_id);
177
178
  void this.#options.gateway.receive({
178
- adapter: this.#options.id,
179
- target,
179
+ conversation,
180
+ message: { conversation, id: formatInboundMessageId(data) },
180
181
  content,
181
182
  segments,
182
183
  sender: String(data.sender_id),
183
- id: formatInboundMessageId(data),
184
184
  metadata: Object.freeze({
185
185
  message_scene: data.message_scene,
186
186
  peer_id: String(data.peer_id),
@@ -1,4 +1,4 @@
1
- import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
1
+ import type { EndpointInstance, EndpointManagement, EndpointSendRequest } from '@zhin.js/adapter';
2
2
  import type { MessageGateway } from '@zhin.js/core/runtime';
3
3
  import type { HttpHost } from '@zhin.js/host-http';
4
4
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
@@ -18,10 +18,7 @@ export declare class MilkyWssEndpoint implements EndpointInstance {
18
18
  open(): void;
19
19
  close(): void;
20
20
  stop(): Promise<void>;
21
- send({ target, payload }: {
22
- readonly target: string;
23
- readonly payload: unknown;
24
- }): Promise<string>;
21
+ send({ conversation, payload }: EndpointSendRequest): Promise<string>;
25
22
  callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
26
23
  recallMessage(id: string): Promise<void>;
27
24
  kickMember(groupId: number, userId: number, rejectAddRequest?: boolean): Promise<boolean>;
@@ -6,7 +6,7 @@ import { formatCompact, getLogger } from '@zhin.js/logger';
6
6
  import { verifyMilkyAccessToken } from './milky-auth.js';
7
7
  import { createMilkyEndpointManagement } from './endpoint-management.js';
8
8
  import { registerMilkyAgentEndpoint } from './milky-agent-deps.js';
9
- import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
9
+ import { buildSendAction, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundSegments, formatOutboundMessageId, formatOutboundSegments, isMentioned, milkyInboundConversation, parseMessageReceiveData, parseMilkyMessageId, senderNickname, } from './protocol.js';
10
10
  const logger = getLogger('milky');
11
11
  const WS_OPEN = 1;
12
12
  export class MilkyWssEndpoint {
@@ -66,15 +66,15 @@ export class MilkyWssEndpoint {
66
66
  }
67
67
  this.#started = false;
68
68
  }
69
- async send({ target, payload }) {
69
+ async send({ conversation, payload }) {
70
70
  const message = formatOutboundSegments(payload);
71
- const { action, params } = buildSendAction(target, message);
71
+ const { action, params } = buildSendAction(conversation, message);
72
72
  const data = await this.callApi(action, params);
73
- const messageId = formatOutboundMessageId(target, data?.message_seq);
73
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
74
74
  logger.debug(formatCompact({
75
75
  op: 'milky_send',
76
76
  endpoint: this.#options.config.name,
77
- target,
77
+ target: `${conversation.kind}:${conversation.id}`,
78
78
  messageId,
79
79
  }));
80
80
  return messageId;
@@ -166,19 +166,19 @@ export class MilkyWssEndpoint {
166
166
  };
167
167
  }
168
168
  #admitMessage(data, event) {
169
- const target = formatInboundTarget(data);
169
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
170
+ const target = `${conversation.kind}:${conversation.id}`;
170
171
  const content = formatInboundContent(data);
171
172
  const segments = formatInboundSegments(data);
172
173
  const audioUrl = extractInboundAudioUrl(data);
173
174
  const nickname = senderNickname(data);
174
175
  const mentioned = isMentioned(data, event.self_id);
175
176
  void this.#options.gateway.receive({
176
- adapter: this.#options.id,
177
- target,
177
+ conversation,
178
+ message: { conversation, id: formatInboundMessageId(data) },
178
179
  content,
179
180
  segments,
180
181
  sender: String(data.sender_id),
181
- id: formatInboundMessageId(data),
182
182
  metadata: Object.freeze({
183
183
  message_scene: data.message_scene,
184
184
  peer_id: String(data.peer_id),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-milky",
3
- "version": "6.0.2",
3
+ "version": "6.0.3",
4
4
  "description": "Zhin.js Milky adapter for Plugin Runtime (WebSocket client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -40,12 +40,13 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "ws": "^8.21.1",
43
- "@zhin.js/adapter": "1.1.4",
44
- "@zhin.js/command": "1.0.6",
45
- "@zhin.js/core": "1.5.1",
46
- "@zhin.js/host-http": "1.0.5",
43
+ "@zhin.js/adapter": "1.1.5",
44
+ "@zhin.js/command": "1.0.7",
45
+ "@zhin.js/core": "1.5.2",
46
+ "@zhin.js/host-http": "1.0.6",
47
+ "@zhin.js/im-contract": "1.0.1",
47
48
  "@zhin.js/logger": "1.0.75",
48
- "@zhin.js/plugin-runtime": "1.1.2"
49
+ "@zhin.js/plugin-runtime": "1.1.3"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/node": "^26.1.2",
@@ -53,16 +54,16 @@
53
54
  "typescript": "^6.0.3",
54
55
  "vitest": "^4.1.10",
55
56
  "zod": "^4.4.3",
56
- "@zhin.js/agent": "1.1.2",
57
- "@zhin.js/host-http": "1.0.5"
57
+ "@zhin.js/agent": "1.1.3",
58
+ "@zhin.js/host-http": "1.0.6"
58
59
  },
59
60
  "peerDependencies": {
60
61
  "zod": "^4.0.0",
61
- "@zhin.js/adapter": "1.1.4",
62
- "@zhin.js/agent": "1.1.2",
63
- "@zhin.js/core": "1.5.1",
64
- "@zhin.js/plugin-runtime": "1.1.2",
65
- "zhin.js": "6.0.1"
62
+ "@zhin.js/adapter": "1.1.5",
63
+ "@zhin.js/agent": "1.1.3",
64
+ "@zhin.js/core": "1.5.2",
65
+ "@zhin.js/plugin-runtime": "1.1.3",
66
+ "zhin.js": "6.0.2"
66
67
  },
67
68
  "peerDependenciesMeta": {
68
69
  "zhin.js": {
package/src/index.ts CHANGED
@@ -5,15 +5,11 @@ export {
5
5
  callApi,
6
6
  extractInboundAudioUrl,
7
7
  formatInboundContent,
8
- formatInboundMessageId,
9
- formatInboundTarget,
10
- formatOutboundMessageId,
11
8
  formatOutboundSegments,
12
9
  isMentioned,
13
10
  isMessageReceiveEvent,
11
+ milkyInboundConversation,
14
12
  parseMessageReceiveData,
15
- parseMilkyMessageId,
16
- parseSendTarget,
17
13
  resolveMilkyConfig,
18
14
  senderNickname,
19
15
  type MilkyAdapterConfig,
@@ -30,7 +26,6 @@ export {
30
26
  type MilkyWireSegment,
31
27
  type MilkyWsConfig,
32
28
  type MilkyWssConfig,
33
- type ParsedSendTarget,
34
29
  type ResolvedMilkyConfig,
35
30
  } from './protocol.js';
36
31
 
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');
@@ -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;
@@ -261,10 +257,22 @@ export function isMessageReceiveEvent(
261
257
  return parseMessageReceiveData(event) != null;
262
258
  }
263
259
 
264
- /** Gateway reply target:`private:uid` / `group:gid` */
265
- export function formatInboundTarget(data: MilkyIncomingMessage): string {
266
- const isGroup = data.message_scene === 'group';
267
- return `${isGroup ? 'group' : 'private'}:${data.peer_id}`;
260
+ /**
261
+ * 入站归一化 ConversationRef:`friend`(私聊)→ `private`;`group` → `group`;
262
+ * `temp`(群临时会话)→ 群容器内的 `private` 会话(parent = 来源群)。
263
+ */
264
+ export function milkyInboundConversation(
265
+ endpointId: string,
266
+ data: MilkyIncomingMessage,
267
+ ): ConversationRef {
268
+ return {
269
+ endpoint: { id: endpointId, adapter: endpointId.split('\0')[0] ?? endpointId },
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
- target: string,
569
+ conversation: ConversationRef,
574
570
  message: MilkyOutgoingSegment[],
575
571
  ): { action: string; params: Record<string, unknown> } {
576
- const parsed = parseSendTarget(target);
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(parsed.id, 10),
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(parsed.id, 10),
584
+ user_id: parseInt(conversation.id, 10),
590
585
  message,
591
586
  },
592
587
  };
593
588
  }
594
589
 
595
- /** message_seq result → gateway message id */
590
+ /**
591
+ * message_seq result → gateway message id(`scene:peer:seq` 复合格式,
592
+ * 与 formatInboundMessageId 同源,仅供 recall 链在本端点边界内解析)。
593
+ */
596
594
  export function formatOutboundMessageId(
597
- target: string,
595
+ conversation: ConversationRef,
598
596
  messageSeq: number | undefined,
599
597
  ): string {
600
598
  if (messageSeq == null) return '';
601
- const parsed = parseSendTarget(target);
602
- const scene = parsed.message_type === 'group' ? 'group' : 'friend';
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(
@@ -7,6 +7,7 @@ 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
13
  import { formatCompact, getLogger } from '@zhin.js/logger';
@@ -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,
@@ -106,15 +107,15 @@ export class MilkySseEndpoint implements EndpointInstance {
106
107
  this.#stream = undefined;
107
108
  }
108
109
 
109
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
110
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
110
111
  const message = formatOutboundSegments(payload);
111
- const { action, params } = buildSendAction(target, message);
112
+ const { action, params } = buildSendAction(conversation, message);
112
113
  const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
113
- const messageId = formatOutboundMessageId(target, data?.message_seq);
114
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
114
115
  logger.debug(formatCompact({
115
116
  op: 'milky_send',
116
117
  endpoint: this.#options.config.name,
117
- target,
118
+ target: `${conversation.kind}:${conversation.id}`,
118
119
  messageId,
119
120
  mode: 'sse',
120
121
  }));
@@ -218,19 +219,19 @@ export class MilkySseEndpoint implements EndpointInstance {
218
219
  }
219
220
 
220
221
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
221
- const target = formatInboundTarget(data);
222
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
223
+ const target = `${conversation.kind}:${conversation.id}`;
222
224
  const content = formatInboundContent(data);
223
225
  const segments = formatInboundSegments(data);
224
226
  const audioUrl = extractInboundAudioUrl(data);
225
227
  const nickname = senderNickname(data);
226
228
  const mentioned = isMentioned(data, event.self_id);
227
229
  void this.#options.gateway.receive({
228
- adapter: this.#options.id,
229
- target,
230
+ conversation,
231
+ message: { conversation, id: formatInboundMessageId(data) },
230
232
  content,
231
233
  segments,
232
234
  sender: String(data.sender_id),
233
- id: formatInboundMessageId(data),
234
235
  metadata: Object.freeze({
235
236
  message_scene: data.message_scene,
236
237
  peer_id: String(data.peer_id),
@@ -2,7 +2,11 @@
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, EndpointManagement } from '@zhin.js/adapter';
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
12
  import { formatCompact, getLogger } from '@zhin.js/logger';
@@ -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,
@@ -83,15 +87,15 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
83
87
  logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
84
88
  }
85
89
 
86
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
90
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
87
91
  const message = formatOutboundSegments(payload);
88
- const { action, params } = buildSendAction(target, message);
92
+ const { action, params } = buildSendAction(conversation, message);
89
93
  const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
90
- const messageId = formatOutboundMessageId(target, data?.message_seq);
94
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
91
95
  logger.debug(formatCompact({
92
96
  op: 'milky_send',
93
97
  endpoint: this.#options.config.name,
94
- target,
98
+ target: `${conversation.kind}:${conversation.id}`,
95
99
  messageId,
96
100
  }));
97
101
  return messageId;
@@ -194,19 +198,19 @@ export class MilkyWebhookEndpoint implements EndpointInstance {
194
198
  }
195
199
 
196
200
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
197
- const target = formatInboundTarget(data);
201
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
202
+ const target = `${conversation.kind}:${conversation.id}`;
198
203
  const content = formatInboundContent(data);
199
204
  const segments = formatInboundSegments(data);
200
205
  const audioUrl = extractInboundAudioUrl(data);
201
206
  const nickname = senderNickname(data);
202
207
  const mentioned = isMentioned(data, event.self_id);
203
208
  void this.#options.gateway.receive({
204
- adapter: this.#options.id,
205
- target,
209
+ conversation,
210
+ message: { conversation, id: formatInboundMessageId(data) },
206
211
  content,
207
212
  segments,
208
213
  sender: String(data.sender_id),
209
- id: formatInboundMessageId(data),
210
214
  metadata: Object.freeze({
211
215
  message_scene: data.message_scene,
212
216
  peer_id: String(data.peer_id),
@@ -8,6 +8,7 @@ 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
14
  import { formatCompact, getLogger } from '@zhin.js/logger';
@@ -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,
@@ -109,15 +110,15 @@ export class MilkyWsEndpoint implements EndpointInstance {
109
110
  }
110
111
  }
111
112
 
112
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
113
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
113
114
  const message = formatOutboundSegments(payload);
114
- const { action, params } = buildSendAction(target, message);
115
+ const { action, params } = buildSendAction(conversation, message);
115
116
  const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
116
- const messageId = formatOutboundMessageId(target, data?.message_seq);
117
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
117
118
  logger.debug(formatCompact({
118
119
  op: 'milky_send',
119
120
  endpoint: this.#options.config.name,
120
- target,
121
+ target: `${conversation.kind}:${conversation.id}`,
121
122
  messageId,
122
123
  }));
123
124
  return messageId;
@@ -222,19 +223,19 @@ export class MilkyWsEndpoint implements EndpointInstance {
222
223
  }
223
224
 
224
225
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
225
- const target = formatInboundTarget(data);
226
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
227
+ const target = `${conversation.kind}:${conversation.id}`;
226
228
  const content = formatInboundContent(data);
227
229
  const segments = formatInboundSegments(data);
228
230
  const audioUrl = extractInboundAudioUrl(data);
229
231
  const nickname = senderNickname(data);
230
232
  const mentioned = isMentioned(data, event.self_id);
231
233
  void this.#options.gateway.receive({
232
- adapter: this.#options.id,
233
- target,
234
+ conversation,
235
+ message: { conversation, id: formatInboundMessageId(data) },
234
236
  content,
235
237
  segments,
236
238
  sender: String(data.sender_id),
237
- id: formatInboundMessageId(data),
238
239
  metadata: Object.freeze({
239
240
  message_scene: data.message_scene,
240
241
  peer_id: String(data.peer_id),
@@ -2,7 +2,11 @@
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, EndpointManagement } from '@zhin.js/adapter';
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, WsConnection } from '@zhin.js/host-http';
8
12
  import { formatCompact, getLogger } from '@zhin.js/logger';
@@ -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,
@@ -102,15 +106,15 @@ export class MilkyWssEndpoint implements EndpointInstance {
102
106
  this.#started = false;
103
107
  }
104
108
 
105
- async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
109
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
106
110
  const message = formatOutboundSegments(payload);
107
- const { action, params } = buildSendAction(target, message);
111
+ const { action, params } = buildSendAction(conversation, message);
108
112
  const data = await this.callApi(action, params) as { message_seq?: number } | undefined;
109
- const messageId = formatOutboundMessageId(target, data?.message_seq);
113
+ const messageId = formatOutboundMessageId(conversation, data?.message_seq);
110
114
  logger.debug(formatCompact({
111
115
  op: 'milky_send',
112
116
  endpoint: this.#options.config.name,
113
- target,
117
+ target: `${conversation.kind}:${conversation.id}`,
114
118
  messageId,
115
119
  }));
116
120
  return messageId;
@@ -213,19 +217,19 @@ export class MilkyWssEndpoint implements EndpointInstance {
213
217
  }
214
218
 
215
219
  #admitMessage(data: MilkyIncomingMessage, event: MilkyEvent): void {
216
- const target = formatInboundTarget(data);
220
+ const conversation = milkyInboundConversation(String(this.#options.id), data);
221
+ const target = `${conversation.kind}:${conversation.id}`;
217
222
  const content = formatInboundContent(data);
218
223
  const segments = formatInboundSegments(data);
219
224
  const audioUrl = extractInboundAudioUrl(data);
220
225
  const nickname = senderNickname(data);
221
226
  const mentioned = isMentioned(data, event.self_id);
222
227
  void this.#options.gateway.receive({
223
- adapter: this.#options.id,
224
- target,
228
+ conversation,
229
+ message: { conversation, id: formatInboundMessageId(data) },
225
230
  content,
226
231
  segments,
227
232
  sender: String(data.sender_id),
228
- id: formatInboundMessageId(data),
229
233
  metadata: Object.freeze({
230
234
  message_scene: data.message_scene,
231
235
  peer_id: String(data.peer_id),