@zhin.js/adapter-milky 3.0.2 → 4.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +443 -0
  2. package/README.md +35 -121
  3. package/adapters/milky.ts +68 -0
  4. package/lib/endpoint.d.ts +7 -0
  5. package/lib/endpoint.js +6 -0
  6. package/lib/index.d.ts +3 -0
  7. package/lib/index.js +3 -0
  8. package/lib/milky-agent-deps.d.ts +24 -0
  9. package/lib/milky-agent-deps.js +30 -0
  10. package/lib/milky-auth.d.ts +3 -0
  11. package/lib/milky-auth.js +30 -0
  12. package/lib/protocol.d.ts +162 -0
  13. package/lib/protocol.js +337 -0
  14. package/lib/sse-client.d.ts +20 -0
  15. package/lib/sse-client.js +85 -0
  16. package/lib/sse-endpoint.d.ts +50 -0
  17. package/lib/sse-endpoint.js +261 -0
  18. package/lib/webhook-endpoint.d.ts +40 -0
  19. package/lib/webhook-endpoint.js +212 -0
  20. package/lib/ws-endpoint.d.ts +42 -0
  21. package/lib/ws-endpoint.js +320 -0
  22. package/lib/ws-types.d.ts +11 -0
  23. package/lib/ws-types.js +1 -0
  24. package/lib/wss-endpoint.d.ts +40 -0
  25. package/lib/wss-endpoint.js +263 -0
  26. package/package.json +43 -20
  27. package/plugin.ts +8 -0
  28. package/schema.json +36 -0
  29. package/src/endpoint.ts +26 -0
  30. package/src/index.ts +59 -57
  31. package/src/milky-agent-deps.ts +44 -8
  32. package/src/milky-auth.ts +29 -0
  33. package/src/protocol.ts +491 -0
  34. package/src/sse-client.ts +106 -0
  35. package/src/sse-endpoint.ts +318 -0
  36. package/src/webhook-endpoint.ts +262 -0
  37. package/src/ws-endpoint.ts +373 -0
  38. package/src/ws-types.ts +12 -0
  39. package/src/wss-endpoint.ts +305 -0
  40. package/lib/src/adapter.js +0 -99
  41. package/lib/src/adapter.js.map +0 -1
  42. package/lib/src/api.js +0 -48
  43. package/lib/src/api.js.map +0 -1
  44. package/lib/src/endpoint-sse.js +0 -197
  45. package/lib/src/endpoint-sse.js.map +0 -1
  46. package/lib/src/endpoint-webhook.js +0 -189
  47. package/lib/src/endpoint-webhook.js.map +0 -1
  48. package/lib/src/endpoint-ws.js +0 -264
  49. package/lib/src/endpoint-ws.js.map +0 -1
  50. package/lib/src/endpoint-wss.js +0 -227
  51. package/lib/src/endpoint-wss.js.map +0 -1
  52. package/lib/src/index.js +0 -45
  53. package/lib/src/index.js.map +0 -1
  54. package/lib/src/milky-agent-deps.js +0 -10
  55. package/lib/src/milky-agent-deps.js.map +0 -1
  56. package/lib/src/segment-mapper.js +0 -2
  57. package/lib/src/segment-mapper.js.map +0 -1
  58. package/lib/src/types.js +0 -5
  59. package/lib/src/types.js.map +0 -1
  60. package/lib/src/utils.js +0 -169
  61. package/lib/src/utils.js.map +0 -1
  62. package/plugin.yml +0 -3
  63. package/src/adapter.ts +0 -111
  64. package/src/api.ts +0 -63
  65. package/src/endpoint-sse.ts +0 -232
  66. package/src/endpoint-webhook.ts +0 -225
  67. package/src/endpoint-ws.ts +0 -300
  68. package/src/endpoint-wss.ts +0 -272
  69. package/src/segment-mapper.ts +0 -1
  70. package/src/types.ts +0 -73
  71. package/src/utils.ts +0 -182
@@ -0,0 +1,7 @@
1
+ export { MilkyWsEndpoint, type MilkyWsEndpointOptions, } from './ws-endpoint.js';
2
+ export { MilkyWebhookEndpoint, type MilkyWebhookEndpointOptions, } from './webhook-endpoint.js';
3
+ export { MilkyWssEndpoint, type MilkyWssEndpointOptions, } from './wss-endpoint.js';
4
+ export { MilkySseEndpoint, type MilkySseEndpointOptions, type CreateMilkySseStream, } from './sse-endpoint.js';
5
+ export type { MilkyWsSocket, MilkyWsCreateOptions } from './ws-types.js';
6
+ export { verifyMilkyAccessToken, readRequestBody } from './milky-auth.js';
7
+ export { openSseStream, consumeSseBuffer } from './sse-client.js';
@@ -0,0 +1,6 @@
1
+ export { MilkyWsEndpoint, } from './ws-endpoint.js';
2
+ export { MilkyWebhookEndpoint, } from './webhook-endpoint.js';
3
+ export { MilkyWssEndpoint, } from './wss-endpoint.js';
4
+ export { MilkySseEndpoint, } from './sse-endpoint.js';
5
+ export { verifyMilkyAccessToken, readRequestBody } from './milky-auth.js';
6
+ export { openSseStream, consumeSseBuffer } from './sse-client.js';
package/lib/index.d.ts ADDED
@@ -0,0 +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';
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
+ export { getMilkyAgentDeps, registerMilkyAgentEndpoint, setMilkyAgentDeps, type MilkyAgentDeps, type MilkyAgentEndpoint, } from './milky-agent-deps.js';
package/lib/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { buildSendAction, buildSseConnectOptions, buildWsConnectOptions, callApi, extractInboundAudioUrl, formatInboundContent, formatInboundMessageId, formatInboundTarget, formatOutboundMessageId, formatOutboundSegments, isMentioned, isMessageReceiveEvent, parseMessageReceiveData, parseMilkyMessageId, parseSendTarget, resolveMilkyConfig, senderNickname, } from './protocol.js';
2
+ export { MilkySseEndpoint, MilkyWebhookEndpoint, MilkyWssEndpoint, MilkyWsEndpoint, consumeSseBuffer, openSseStream, readRequestBody, verifyMilkyAccessToken, } from './endpoint.js';
3
+ export { getMilkyAgentDeps, registerMilkyAgentEndpoint, setMilkyAgentDeps, } from './milky-agent-deps.js';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Agent tool deps for milky (scene management / callApi).
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
+ */
5
+ export interface MilkyAgentEndpoint {
6
+ callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
7
+ kickMember(groupId: number, userId: number, rejectAddRequest?: boolean): Promise<boolean>;
8
+ muteMember(groupId: number, userId: number, duration?: number): Promise<boolean>;
9
+ muteAll(groupId: number, enable?: boolean): Promise<boolean>;
10
+ setAdmin(groupId: number, userId: number, enable?: boolean): Promise<boolean>;
11
+ setCard(groupId: number, userId: number, card: string): Promise<boolean>;
12
+ setTitle(groupId: number, userId: number, title: string): Promise<boolean>;
13
+ setGroupName(groupId: number, name: string): Promise<boolean>;
14
+ getMemberList(groupId: number): Promise<unknown[]>;
15
+ getGroupInfo(groupId: number): Promise<unknown>;
16
+ recallMessage?(id: string): Promise<void>;
17
+ }
18
+ export interface MilkyAgentDeps {
19
+ getEndpoint: (endpointId: string) => MilkyAgentEndpoint;
20
+ }
21
+ export declare function registerMilkyAgentEndpoint(endpointId: string, endpoint: MilkyAgentEndpoint): () => void;
22
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
23
+ export declare function setMilkyAgentDeps(deps: MilkyAgentDeps | null): void;
24
+ export declare function getMilkyAgentDeps(): MilkyAgentDeps;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Agent tool deps for milky (scene management / callApi).
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
+ */
5
+ const endpoints = new Map();
6
+ let override = null;
7
+ export function registerMilkyAgentEndpoint(endpointId, endpoint) {
8
+ endpoints.set(endpointId, endpoint);
9
+ return () => {
10
+ if (endpoints.get(endpointId) === endpoint) {
11
+ endpoints.delete(endpointId);
12
+ }
13
+ };
14
+ }
15
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
16
+ export function setMilkyAgentDeps(deps) {
17
+ override = deps;
18
+ }
19
+ export function getMilkyAgentDeps() {
20
+ if (override)
21
+ return override;
22
+ return {
23
+ getEndpoint(endpointId) {
24
+ const registered = endpoints.get(endpointId);
25
+ if (!registered)
26
+ throw new Error(`Endpoint ${endpointId} 不存在`);
27
+ return registered;
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,3 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ export declare function verifyMilkyAccessToken(accessToken: string | undefined, request: IncomingMessage): boolean;
3
+ export declare function readRequestBody(request: IncomingMessage): Promise<string>;
@@ -0,0 +1,30 @@
1
+ export function verifyMilkyAccessToken(accessToken, request) {
2
+ if (!accessToken)
3
+ return true;
4
+ const auth = request.headers.authorization ?? '';
5
+ if (auth === `Bearer ${accessToken}`)
6
+ return true;
7
+ try {
8
+ const url = new URL(request.url ?? '/', 'http://localhost');
9
+ if (url.searchParams.get('access_token') === accessToken)
10
+ return true;
11
+ }
12
+ catch {
13
+ /* ignore */
14
+ }
15
+ return false;
16
+ }
17
+ export async function readRequestBody(request) {
18
+ const chunks = [];
19
+ let size = 0;
20
+ for await (const chunk of request) {
21
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
22
+ size += buffer.length;
23
+ if (size > 1_048_576) {
24
+ request.destroy();
25
+ throw new Error('Request body exceeds 1MB');
26
+ }
27
+ chunks.push(buffer);
28
+ }
29
+ return Buffer.concat(chunks).toString('utf8');
30
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Milky protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ * Spec: https://milky.ntqqrev.org/
5
+ */
6
+ /** Transitional legacy endpoint row (`endpoints[]` with `context: milky`). */
7
+ export interface MilkyLegacyEndpointRow {
8
+ readonly context?: string;
9
+ readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
10
+ readonly name?: string;
11
+ readonly baseUrl?: string;
12
+ readonly access_token?: string;
13
+ readonly path?: string;
14
+ readonly reconnect_interval?: number;
15
+ readonly heartbeat_interval?: number;
16
+ }
17
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
18
+ export interface MilkyAdapterConfig {
19
+ readonly connection?: 'ws' | 'sse' | 'webhook' | 'wss';
20
+ readonly name?: string;
21
+ readonly baseUrl?: string;
22
+ readonly access_token?: string;
23
+ readonly path?: string;
24
+ readonly reconnect_interval?: number;
25
+ readonly heartbeat_interval?: number;
26
+ /** Transitional: legacy root `endpoints[]` with `context: milky`. */
27
+ readonly endpoints?: ReadonlyArray<MilkyLegacyEndpointRow>;
28
+ }
29
+ export interface MilkyConfigBase {
30
+ readonly context: 'milky';
31
+ readonly name: string;
32
+ readonly baseUrl: string;
33
+ readonly access_token?: string;
34
+ }
35
+ /** WebSocket 正向:应用连协议端 ws(s)://baseUrl/event */
36
+ export interface MilkyWsConfig extends MilkyConfigBase {
37
+ readonly connection: 'ws';
38
+ readonly reconnect_interval: number;
39
+ readonly heartbeat_interval: number;
40
+ }
41
+ /** SSE:HTTP GET `text/event-stream` on `/event`(同 WS 事件流) */
42
+ export interface MilkySseConfig extends MilkyConfigBase {
43
+ readonly connection: 'sse';
44
+ readonly reconnect_interval: number;
45
+ }
46
+ /** Webhook:httpHostToken POST 入站 + baseUrl HTTP API 出站 */
47
+ export interface MilkyWebhookConfig extends MilkyConfigBase {
48
+ readonly connection: 'webhook';
49
+ readonly path: string;
50
+ }
51
+ /** 反向 WS:httpHostToken WS upgrade 入站 + baseUrl HTTP API 出站 */
52
+ export interface MilkyWssConfig extends MilkyConfigBase {
53
+ readonly connection: 'wss';
54
+ readonly path: string;
55
+ readonly heartbeat_interval: number;
56
+ }
57
+ export type ResolvedMilkyConfig = MilkyWsConfig | MilkySseConfig | MilkyWebhookConfig | MilkyWssConfig;
58
+ export type MilkyEndpointConfig = ResolvedMilkyConfig;
59
+ export interface MilkyApiResponse<T = unknown> {
60
+ status: string;
61
+ retcode: number;
62
+ data?: T;
63
+ message?: string;
64
+ }
65
+ export interface MilkyEvent {
66
+ event_type: string;
67
+ time: number;
68
+ self_id: number;
69
+ data?: Record<string, unknown>;
70
+ }
71
+ export interface MilkyIncomingMessage {
72
+ message_scene: 'friend' | 'group' | 'temp';
73
+ peer_id: number;
74
+ message_seq: number;
75
+ sender_id: number;
76
+ time: number;
77
+ segments: MilkyIncomingSegment[];
78
+ friend?: {
79
+ user_id: number;
80
+ nickname?: string;
81
+ };
82
+ group?: {
83
+ group_id: number;
84
+ group_name?: string;
85
+ };
86
+ group_member?: {
87
+ user_id: number;
88
+ nickname?: string;
89
+ card?: string;
90
+ role?: string;
91
+ };
92
+ }
93
+ export interface MilkyIncomingSegment {
94
+ type: string;
95
+ data?: Record<string, unknown>;
96
+ }
97
+ export interface MilkyWireSegment {
98
+ readonly type: string;
99
+ readonly data?: Record<string, unknown>;
100
+ }
101
+ export interface MilkyOutgoingSegment {
102
+ type: string;
103
+ data: Record<string, unknown>;
104
+ }
105
+ export interface ParsedSendTarget {
106
+ readonly message_type: 'private' | 'group';
107
+ readonly id: string;
108
+ }
109
+ export interface MilkyApiClientOptions {
110
+ readonly baseUrl: string;
111
+ readonly access_token?: string;
112
+ }
113
+ export declare function resolveMilkyConfig(config?: MilkyAdapterConfig): ResolvedMilkyConfig;
114
+ /**
115
+ * 调用协议端 API:POST {baseUrl}/api/{apiName},Body JSON,鉴权。
116
+ * 非 200 或 retcode !== 0 时抛错。
117
+ */
118
+ export declare function callApi<T = unknown>(options: MilkyApiClientOptions, apiName: string, params?: Record<string, unknown>): Promise<T>;
119
+ /** 根据 event_type 判断是否为 message_receive,并解析 data */
120
+ export declare function parseMessageReceiveData(event: MilkyEvent): MilkyIncomingMessage | null;
121
+ export declare function isMessageReceiveEvent(event: MilkyEvent): event is MilkyEvent & {
122
+ data: MilkyIncomingMessage;
123
+ };
124
+ /** Gateway reply target:`private:uid` / `group:gid` */
125
+ export declare function formatInboundTarget(data: MilkyIncomingMessage): string;
126
+ export declare function formatInboundContent(data: MilkyIncomingMessage): string;
127
+ /** First audio URI from inbound segments (for Agent Host STT preprocess). */
128
+ export declare function extractInboundAudioUrl(data: MilkyIncomingMessage): string | undefined;
129
+ /** 发送者显示名(群名片/群昵称/好友昵称);没有可靠的显示名时返回 undefined。 */
130
+ export declare function senderNickname(data: MilkyIncomingMessage): string | undefined;
131
+ /** 消息段中含 @ 本机(mention 段 user_id 等于协议端上报的 self_id)。 */
132
+ export declare function isMentioned(data: MilkyIncomingMessage, selfId: number | undefined): boolean;
133
+ export declare function formatInboundMessageId(data: MilkyIncomingMessage): string;
134
+ export declare function parseSendTarget(target: string): ParsedSendTarget;
135
+ /**
136
+ * Wire-encode an already-rendered outbound payload into Milky outgoing segments.
137
+ * Segment canonicalization is intentionally not done here.
138
+ */
139
+ export declare function formatOutboundSegments(payload: unknown): MilkyOutgoingSegment[];
140
+ export declare function buildSendAction(target: string, message: MilkyOutgoingSegment[]): {
141
+ action: string;
142
+ params: Record<string, unknown>;
143
+ };
144
+ /** message_seq result → gateway message id */
145
+ export declare function formatOutboundMessageId(target: string, messageSeq: number | undefined): string;
146
+ export declare function parseMilkyMessageId(msgId: string): {
147
+ message_scene: 'friend' | 'group' | 'temp';
148
+ peer_id: number;
149
+ message_seq: number;
150
+ } | null;
151
+ /** Build WS event URL + headers (access_token via Bearer + query). */
152
+ export declare function buildWsConnectOptions(config: MilkyWsConfig): {
153
+ readonly url: string;
154
+ readonly headers: Record<string, string>;
155
+ readonly safeUrl: string;
156
+ };
157
+ /** Build SSE event URL (HTTP) + headers. */
158
+ export declare function buildSseConnectOptions(config: MilkySseConfig): {
159
+ readonly url: string;
160
+ readonly headers: Record<string, string>;
161
+ readonly safeUrl: string;
162
+ };
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Milky protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ * Spec: https://milky.ntqqrev.org/
5
+ */
6
+ function normalizeConnection(connection) {
7
+ if (connection === 'sse' || connection === 'webhook' || connection === 'wss')
8
+ return connection;
9
+ return 'ws';
10
+ }
11
+ export function resolveMilkyConfig(config = {}) {
12
+ const entry = config.endpoints?.find((item) => item.context === 'milky');
13
+ const connection = normalizeConnection(config.connection ?? entry?.connection);
14
+ const name = (typeof config.name === 'string' && config.name)
15
+ || (typeof entry?.name === 'string' && entry.name)
16
+ || process.env.MILKY_BOT_NAME
17
+ || 'milky-bot';
18
+ const baseUrl = config.baseUrl ?? entry?.baseUrl;
19
+ if (!baseUrl) {
20
+ throw new TypeError('Milky requires baseUrl (plugins.<key>.baseUrl or endpoints with context: milky)');
21
+ }
22
+ const access_token = config.access_token ?? entry?.access_token;
23
+ const base = {
24
+ context: 'milky',
25
+ name,
26
+ baseUrl,
27
+ access_token,
28
+ };
29
+ if (connection === 'ws') {
30
+ return {
31
+ ...base,
32
+ connection: 'ws',
33
+ reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
34
+ heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
35
+ };
36
+ }
37
+ if (connection === 'sse') {
38
+ return {
39
+ ...base,
40
+ connection: 'sse',
41
+ reconnect_interval: config.reconnect_interval
42
+ ?? entry?.reconnect_interval
43
+ ?? 5_000,
44
+ };
45
+ }
46
+ if (connection === 'webhook') {
47
+ const path = config.path ?? entry?.path;
48
+ if (!path)
49
+ throw new TypeError('Milky connection:webhook requires path');
50
+ return { ...base, connection: 'webhook', path };
51
+ }
52
+ if (connection === 'wss') {
53
+ const path = config.path ?? entry?.path;
54
+ if (!path)
55
+ throw new TypeError('Milky connection:wss requires path');
56
+ return {
57
+ ...base,
58
+ connection: 'wss',
59
+ path,
60
+ heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
61
+ };
62
+ }
63
+ throw new TypeError(`Unknown Milky connection: ${String(connection)}`);
64
+ }
65
+ /** 鉴权:Header Authorization: Bearer {token} 或 URL query access_token=xxx */
66
+ function authHeaders(access_token) {
67
+ const headers = {};
68
+ if (access_token) {
69
+ headers.Authorization = `Bearer ${access_token}`;
70
+ }
71
+ return headers;
72
+ }
73
+ function authQuery(access_token) {
74
+ if (!access_token)
75
+ return '';
76
+ return `access_token=${encodeURIComponent(access_token)}`;
77
+ }
78
+ /**
79
+ * 调用协议端 API:POST {baseUrl}/api/{apiName},Body JSON,鉴权。
80
+ * 非 200 或 retcode !== 0 时抛错。
81
+ */
82
+ export async function callApi(options, apiName, params = {}) {
83
+ const { baseUrl, access_token } = options;
84
+ const url = new URL(`/api/${apiName}`, baseUrl.replace(/\/$/, ''));
85
+ const q = authQuery(access_token);
86
+ if (q)
87
+ url.search = (url.search ? `${url.search}&` : '') + q;
88
+ const res = await fetch(url.toString(), {
89
+ method: 'POST',
90
+ headers: {
91
+ 'Content-Type': 'application/json',
92
+ ...authHeaders(access_token),
93
+ },
94
+ body: JSON.stringify(Object.keys(params).length ? params : {}),
95
+ });
96
+ const text = await res.text();
97
+ let body;
98
+ try {
99
+ body = JSON.parse(text);
100
+ }
101
+ catch {
102
+ throw new Error(`Milky API ${apiName}: invalid JSON response (${res.status}) ${text.slice(0, 200)}`);
103
+ }
104
+ if (res.status !== 200) {
105
+ throw new Error(`Milky API ${apiName}: HTTP ${res.status} ${body.message ?? text}`);
106
+ }
107
+ if (body.retcode !== 0) {
108
+ throw new Error(`Milky API ${apiName}: retcode=${body.retcode} ${body.message ?? ''}`);
109
+ }
110
+ return (body.data ?? {});
111
+ }
112
+ /** 根据 event_type 判断是否为 message_receive,并解析 data */
113
+ export function parseMessageReceiveData(event) {
114
+ if (event.event_type !== 'message_receive' || !event.data)
115
+ return null;
116
+ const data = event.data;
117
+ if (!data.message_scene || !Number.isInteger(data.peer_id) || !Array.isArray(data.segments)) {
118
+ return null;
119
+ }
120
+ return data;
121
+ }
122
+ export function isMessageReceiveEvent(event) {
123
+ return parseMessageReceiveData(event) != null;
124
+ }
125
+ /** Gateway reply target:`private:uid` / `group:gid` */
126
+ export function formatInboundTarget(data) {
127
+ const isGroup = data.message_scene === 'group';
128
+ return `${isGroup ? 'group' : 'private'}:${data.peer_id}`;
129
+ }
130
+ export function formatInboundContent(data) {
131
+ return data.segments.map((seg) => {
132
+ if (seg.type === 'text')
133
+ return String(seg.data?.text ?? '');
134
+ if (seg.type === 'record' || seg.type === 'audio') {
135
+ const uri = String(seg.data?.uri ?? seg.data?.url ?? '');
136
+ return uri ? `[audio:${uri}]` : '[audio]';
137
+ }
138
+ return '';
139
+ }).join('');
140
+ }
141
+ /** First audio URI from inbound segments (for Agent Host STT preprocess). */
142
+ export function extractInboundAudioUrl(data) {
143
+ for (const seg of data.segments) {
144
+ if (seg.type !== 'record' && seg.type !== 'audio')
145
+ continue;
146
+ const uri = String(seg.data?.uri ?? seg.data?.url ?? '').trim();
147
+ if (uri)
148
+ return uri;
149
+ }
150
+ return undefined;
151
+ }
152
+ /** 发送者显示名(群名片/群昵称/好友昵称);没有可靠的显示名时返回 undefined。 */
153
+ export function senderNickname(data) {
154
+ const isGroup = data.message_scene === 'group';
155
+ const name = isGroup
156
+ ? data.group_member?.card ?? data.group_member?.nickname
157
+ : data.friend?.nickname;
158
+ return typeof name === 'string' && name ? name : undefined;
159
+ }
160
+ /** 消息段中含 @ 本机(mention 段 user_id 等于协议端上报的 self_id)。 */
161
+ export function isMentioned(data, selfId) {
162
+ if (selfId == null)
163
+ return false;
164
+ return data.segments.some((seg) => seg.type === 'mention' && Number(seg.data?.user_id) === selfId);
165
+ }
166
+ export function formatInboundMessageId(data) {
167
+ return `${data.message_scene}:${data.peer_id}:${data.message_seq}`;
168
+ }
169
+ export function parseSendTarget(target) {
170
+ const sep = target.indexOf(':');
171
+ if (sep <= 0) {
172
+ return { message_type: 'private', id: target };
173
+ }
174
+ const head = target.slice(0, sep);
175
+ const rest = target.slice(sep + 1);
176
+ if (head === 'group')
177
+ return { message_type: 'group', id: rest };
178
+ if (head === 'private')
179
+ return { message_type: 'private', id: rest };
180
+ return { message_type: 'private', id: target };
181
+ }
182
+ /**
183
+ * Wire-encode an already-rendered outbound payload into Milky outgoing segments.
184
+ * Segment canonicalization is intentionally not done here.
185
+ */
186
+ export function formatOutboundSegments(payload) {
187
+ if (typeof payload === 'string') {
188
+ return [{ type: 'text', data: { text: payload } }];
189
+ }
190
+ const items = Array.isArray(payload)
191
+ ? payload
192
+ : payload && typeof payload === 'object' && 'type' in payload
193
+ ? [payload]
194
+ : [];
195
+ if (items.length === 0) {
196
+ const text = payload == null
197
+ ? ''
198
+ : typeof payload === 'object'
199
+ ? JSON.stringify(payload)
200
+ : String(payload);
201
+ return [{ type: 'text', data: { text } }];
202
+ }
203
+ const out = [];
204
+ for (const item of items) {
205
+ if (typeof item === 'string') {
206
+ out.push({ type: 'text', data: { text: item } });
207
+ continue;
208
+ }
209
+ const type = item.type;
210
+ const data = item.data ?? {};
211
+ switch (type) {
212
+ case 'text':
213
+ out.push({ type: 'text', data: { text: String(data.text ?? '') } });
214
+ break;
215
+ case 'at':
216
+ if (data.type === 'all') {
217
+ out.push({ type: 'mention_all', data: {} });
218
+ }
219
+ else {
220
+ const id = data.id;
221
+ if (id)
222
+ out.push({ type: 'mention', data: { user_id: Number(id) || 0 } });
223
+ }
224
+ break;
225
+ case 'mention':
226
+ out.push({ type: 'mention', data: { user_id: Number(data.user_id ?? data.id ?? 0) || 0 } });
227
+ break;
228
+ case 'mention_all':
229
+ out.push({ type: 'mention_all', data: {} });
230
+ break;
231
+ case 'face':
232
+ out.push({
233
+ type: 'face',
234
+ data: { face_id: String(data.face_id ?? data.id ?? ''), is_large: false },
235
+ });
236
+ break;
237
+ case 'reply': {
238
+ const mid = data.message_id ?? data.message_seq;
239
+ const seq = typeof mid === 'number'
240
+ ? mid
241
+ : parseInt(String(mid).split(':').pop() ?? '0', 10);
242
+ out.push({ type: 'reply', data: { message_seq: seq } });
243
+ break;
244
+ }
245
+ case 'image':
246
+ out.push({
247
+ type: 'image',
248
+ data: { uri: String(data.url ?? data.uri ?? '') },
249
+ });
250
+ break;
251
+ case 'record':
252
+ out.push({
253
+ type: 'record',
254
+ data: { uri: String(data.url ?? data.uri ?? '') },
255
+ });
256
+ break;
257
+ case 'video':
258
+ out.push({
259
+ type: 'video',
260
+ data: {
261
+ uri: String(data.url ?? data.uri ?? ''),
262
+ thumb_uri: data.thumb_uri,
263
+ },
264
+ });
265
+ break;
266
+ default:
267
+ out.push({ type, data: data });
268
+ }
269
+ }
270
+ return out.length ? out : [{ type: 'text', data: { text: '' } }];
271
+ }
272
+ export function buildSendAction(target, message) {
273
+ const parsed = parseSendTarget(target);
274
+ if (parsed.message_type === 'group') {
275
+ return {
276
+ action: 'send_group_message',
277
+ params: {
278
+ group_id: parseInt(parsed.id, 10),
279
+ message,
280
+ },
281
+ };
282
+ }
283
+ return {
284
+ action: 'send_private_message',
285
+ params: {
286
+ user_id: parseInt(parsed.id, 10),
287
+ message,
288
+ },
289
+ };
290
+ }
291
+ /** message_seq result → gateway message id */
292
+ export function formatOutboundMessageId(target, messageSeq) {
293
+ if (messageSeq == null)
294
+ return '';
295
+ const parsed = parseSendTarget(target);
296
+ const scene = parsed.message_type === 'group' ? 'group' : 'friend';
297
+ return `${scene}:${parsed.id}:${messageSeq}`;
298
+ }
299
+ export function parseMilkyMessageId(msgId) {
300
+ const parts = msgId.split(':');
301
+ if (parts.length < 3)
302
+ return null;
303
+ const [scene, peer, seq] = parts;
304
+ if (scene !== 'friend' && scene !== 'group' && scene !== 'temp')
305
+ return null;
306
+ const peerId = parseInt(peer, 10);
307
+ const messageSeq = parseInt(seq, 10);
308
+ if (Number.isNaN(peerId) || Number.isNaN(messageSeq))
309
+ return null;
310
+ return { message_scene: scene, peer_id: peerId, message_seq: messageSeq };
311
+ }
312
+ /** Build WS event URL + headers (access_token via Bearer + query). */
313
+ export function buildWsConnectOptions(config) {
314
+ const base = config.baseUrl.replace(/\/$/, '');
315
+ let url = `${base.replace(/^http/, 'ws')}/event`;
316
+ const headers = {};
317
+ if (config.access_token) {
318
+ headers.Authorization = `Bearer ${config.access_token}`;
319
+ url = `${url}${url.includes('?') ? '&' : '?'}access_token=${encodeURIComponent(config.access_token)}`;
320
+ }
321
+ const safeUrl = url.replace(/([?&]access_token=)[^&]*/g, '$1***');
322
+ return { url, headers, safeUrl };
323
+ }
324
+ /** Build SSE event URL (HTTP) + headers. */
325
+ export function buildSseConnectOptions(config) {
326
+ const base = config.baseUrl.replace(/\/$/, '');
327
+ let url = `${base}/event`;
328
+ const headers = {
329
+ Accept: 'text/event-stream',
330
+ };
331
+ if (config.access_token) {
332
+ headers.Authorization = `Bearer ${config.access_token}`;
333
+ url = `${url}${url.includes('?') ? '&' : '?'}access_token=${encodeURIComponent(config.access_token)}`;
334
+ }
335
+ const safeUrl = url.replace(/([?&]access_token=)[^&]*/g, '$1***');
336
+ return { url, headers, safeUrl };
337
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Minimal fetch-based SSE client (no EventSource dependency).
3
+ * Parses `text/event-stream` frames and invokes onMessage for each `data:` payload.
4
+ */
5
+ export interface SseClientOptions {
6
+ readonly url: string;
7
+ readonly headers?: Record<string, string>;
8
+ readonly fetch?: typeof globalThis.fetch;
9
+ readonly signal?: AbortSignal;
10
+ readonly onMessage: (data: string) => void;
11
+ readonly onError?: (error: Error) => void;
12
+ readonly onOpen?: () => void;
13
+ }
14
+ export interface SseClientHandle {
15
+ readonly closed: Promise<void>;
16
+ close(): void;
17
+ }
18
+ export declare function openSseStream(options: SseClientOptions): SseClientHandle;
19
+ /** Exported for unit tests. */
20
+ export declare function consumeSseBuffer(buffer: string, onMessage: (data: string) => void): string;