@zhin.js/adapter-milky 1.0.0 → 1.1.0

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 (100) hide show
  1. package/CHANGELOG.md +962 -0
  2. package/README.md +58 -117
  3. package/adapters/milky.js +51 -0
  4. package/adapters/milky.ts +74 -0
  5. package/agent/PERMITS.md +14 -0
  6. package/{skills/milky/SKILL.md → agent/skills/milky.md} +1 -1
  7. package/commands/endpoint/add/[id].js +3 -0
  8. package/commands/endpoint/add/[id].ts +3 -0
  9. package/commands/endpoint/list.js +3 -0
  10. package/commands/endpoint/list.ts +3 -0
  11. package/commands/endpoint/remove/[id].js +3 -0
  12. package/commands/endpoint/remove/[id].ts +3 -0
  13. package/lib/client.d.ts +21 -0
  14. package/lib/client.js +33 -0
  15. package/lib/endpoint-management.d.ts +17 -0
  16. package/lib/endpoint-management.js +47 -0
  17. package/lib/endpoint.d.ts +7 -0
  18. package/lib/endpoint.js +6 -0
  19. package/lib/index.d.ts +6 -20
  20. package/lib/index.js +4 -33
  21. package/lib/milky-auth.d.ts +2 -0
  22. package/lib/milky-auth.js +16 -0
  23. package/lib/milky-endpoint-commands.d.ts +1 -0
  24. package/lib/milky-endpoint-commands.js +18 -0
  25. package/lib/milky-runtime-state.d.ts +1 -0
  26. package/lib/milky-runtime-state.js +6 -0
  27. package/lib/protocol.d.ts +174 -0
  28. package/lib/protocol.js +494 -0
  29. package/lib/sse-client.d.ts +20 -0
  30. package/lib/sse-client.js +85 -0
  31. package/lib/sse-endpoint.d.ts +32 -0
  32. package/lib/sse-endpoint.js +207 -0
  33. package/lib/webhook-endpoint.d.ts +22 -0
  34. package/lib/webhook-endpoint.js +149 -0
  35. package/lib/ws-endpoint.d.ts +23 -0
  36. package/lib/ws-endpoint.js +233 -0
  37. package/lib/ws-types.d.ts +11 -0
  38. package/lib/ws-types.js +1 -0
  39. package/lib/wss-endpoint.d.ts +25 -0
  40. package/lib/wss-endpoint.js +184 -0
  41. package/package.json +73 -19
  42. package/plugin.js +14 -0
  43. package/schema.json +100 -0
  44. package/src/client.ts +80 -0
  45. package/src/endpoint-management.ts +74 -0
  46. package/src/endpoint.ts +26 -0
  47. package/src/index.ts +63 -45
  48. package/src/milky-auth.ts +14 -0
  49. package/src/milky-endpoint-commands.ts +19 -0
  50. package/src/milky-runtime-state.ts +7 -0
  51. package/src/protocol.ts +647 -0
  52. package/src/sse-client.ts +106 -0
  53. package/src/sse-endpoint.ts +258 -0
  54. package/src/webhook-endpoint.ts +196 -0
  55. package/src/ws-endpoint.ts +291 -0
  56. package/src/ws-types.ts +12 -0
  57. package/src/wss-endpoint.ts +228 -0
  58. package/lib/adapter.d.ts +0 -28
  59. package/lib/adapter.d.ts.map +0 -1
  60. package/lib/adapter.js +0 -90
  61. package/lib/adapter.js.map +0 -1
  62. package/lib/api.d.ts +0 -10
  63. package/lib/api.d.ts.map +0 -1
  64. package/lib/api.js +0 -48
  65. package/lib/api.js.map +0 -1
  66. package/lib/bot-sse.d.ts +0 -31
  67. package/lib/bot-sse.d.ts.map +0 -1
  68. package/lib/bot-sse.js +0 -195
  69. package/lib/bot-sse.js.map +0 -1
  70. package/lib/bot-webhook.d.ts +0 -34
  71. package/lib/bot-webhook.d.ts.map +0 -1
  72. package/lib/bot-webhook.js +0 -187
  73. package/lib/bot-webhook.js.map +0 -1
  74. package/lib/bot-ws.d.ts +0 -35
  75. package/lib/bot-ws.d.ts.map +0 -1
  76. package/lib/bot-ws.js +0 -262
  77. package/lib/bot-ws.js.map +0 -1
  78. package/lib/bot-wss.d.ts +0 -34
  79. package/lib/bot-wss.d.ts.map +0 -1
  80. package/lib/bot-wss.js +0 -225
  81. package/lib/bot-wss.js.map +0 -1
  82. package/lib/index.d.ts.map +0 -1
  83. package/lib/index.js.map +0 -1
  84. package/lib/types.d.ts +0 -75
  85. package/lib/types.d.ts.map +0 -1
  86. package/lib/types.js +0 -5
  87. package/lib/types.js.map +0 -1
  88. package/lib/utils.d.ts +0 -35
  89. package/lib/utils.d.ts.map +0 -1
  90. package/lib/utils.js +0 -157
  91. package/lib/utils.js.map +0 -1
  92. package/plugin.yml +0 -3
  93. package/src/adapter.ts +0 -101
  94. package/src/api.ts +0 -63
  95. package/src/bot-sse.ts +0 -228
  96. package/src/bot-webhook.ts +0 -221
  97. package/src/bot-ws.ts +0 -296
  98. package/src/bot-wss.ts +0 -268
  99. package/src/types.ts +0 -73
  100. package/src/utils.ts +0 -174
package/src/bot-wss.ts DELETED
@@ -1,268 +0,0 @@
1
- /**
2
- * Milky 反向 WebSocket Bot:应用开 WS 服务端,协议端来连;鉴权后收事件同正向
3
- */
4
- import { IncomingMessage } from 'http';
5
- import WebSocket, { WebSocketServer } from 'ws';
6
- import { EventEmitter } from 'events';
7
- import { clearInterval } from 'node:timers';
8
- import { formatCompact, Bot, Message, segment, SendOptions } from 'zhin.js';
9
- import type { Router } from '@zhin.js/host-router';
10
- import { callApi } from './api.js';
11
- import type { MilkyWssConfig, MilkyEvent } from './types.js';
12
- import type { MilkyAdapter } from './adapter.js';
13
- import {
14
- formatMilkyMessagePayload,
15
- parseMessageReceiveData,
16
- toMilkyOutgoingSegments,
17
- parseMilkyMessageId,
18
- } from './utils.js';
19
-
20
- function getAccessTokenFromWsRequest(req: IncomingMessage): string | undefined {
21
- const auth = req.headers['authorization'];
22
- if (typeof auth === 'string' && auth.startsWith('Bearer ')) return auth.slice(7);
23
- const url = req.url ?? '';
24
- const idx = url.indexOf('?');
25
- if (idx >= 0) {
26
- const params = new URLSearchParams(url.slice(idx));
27
- return params.get('access_token') ?? undefined;
28
- }
29
- return undefined;
30
- }
31
-
32
- export class MilkyWssServer extends EventEmitter implements Bot<MilkyWssConfig, MilkyEvent> {
33
- $connected: boolean;
34
- #wss?: WebSocketServer;
35
- #client?: WebSocket;
36
- private heartbeatTimer?: NodeJS.Timeout;
37
-
38
- get logger() {
39
- return this.adapter.plugin.logger;
40
- }
41
-
42
- constructor(
43
- public adapter: MilkyAdapter,
44
- public router: Router,
45
- public $config: MilkyWssConfig,
46
- ) {
47
- super();
48
- this.$connected = false;
49
- }
50
-
51
- get $id() {
52
- return this.$config.name;
53
- }
54
-
55
- private apiOptions() {
56
- return { baseUrl: this.$config.baseUrl, access_token: this.$config.access_token };
57
- }
58
-
59
- async $connect(): Promise<void> {
60
- const path = this.$config.path.startsWith('/') ? this.$config.path : `/${this.$config.path}`;
61
- const token = this.$config.access_token;
62
- if (!token) this.logger.warn(formatCompact({ bot: this.$id, ok: false, error: 'missing access_token' }));
63
-
64
- this.#wss = this.router.ws(path, {
65
- verifyClient: (info: { req: IncomingMessage }) => {
66
- const received = getAccessTokenFromWsRequest(info.req);
67
- if (token && received !== token) {
68
- this.logger.error('反向 WS 鉴权失败');
69
- return false;
70
- }
71
- return true;
72
- },
73
- });
74
-
75
- this.$connected = true;
76
- this.logger.info(formatCompact( { op: 'listen', bot: this.$id, mode: 'wss', path }));
77
-
78
- this.#wss.on('connection', (client, req) => {
79
- this.#client = client;
80
- this.startHeartbeat();
81
- this.logger.info(formatCompact({ bot: this.$id, peer: req.socket?.remoteAddress }));
82
-
83
- client.on('message', (data) => {
84
- try {
85
- const event = JSON.parse(data.toString()) as MilkyEvent;
86
- this.handleEvent(event);
87
- } catch (err) {
88
- this.emit('error', err);
89
- }
90
- });
91
-
92
- client.on('close', () => {
93
- this.#client = undefined;
94
- this.logger.warn(formatCompact( { op: 'disconnect', bot: this.$id }));
95
- });
96
-
97
- client.on('error', (err) => this.logger.error('反向 WS 连接错误', err));
98
- });
99
- }
100
-
101
- async $disconnect(): Promise<void> {
102
- if (this.heartbeatTimer) {
103
- clearInterval(this.heartbeatTimer);
104
- this.heartbeatTimer = undefined;
105
- }
106
- this.#wss?.close();
107
- this.#wss = undefined;
108
- this.#client = undefined;
109
- this.$connected = false;
110
- }
111
-
112
- $formatMessage(event: MilkyEvent): Message<MilkyEvent> {
113
- const data = parseMessageReceiveData(event);
114
- if (!data) {
115
- return Message.from(event, {
116
- $id: '',
117
- $adapter: 'milky',
118
- $bot: this.$config.name,
119
- $channel: { id: '', type: 'private' },
120
- $sender: { id: '', name: '' },
121
- $content: [],
122
- $raw: '',
123
- $timestamp: event.time ?? 0,
124
- $recall: async () => {},
125
- $reply: async () => '',
126
- });
127
- }
128
- const payload = formatMilkyMessagePayload(
129
- event,
130
- data,
131
- (id) => this.$recallMessage(id),
132
- (channel, content) =>
133
- this.adapter.sendMessage({
134
- ...channel,
135
- context: 'milky',
136
- bot: this.$config.name,
137
- content: content as import('zhin.js').SendContent,
138
- }),
139
- 'milky',
140
- this.$config.name,
141
- );
142
- return Message.from(event, payload);
143
- }
144
-
145
- private handleEvent(event: MilkyEvent): void {
146
- const data = parseMessageReceiveData(event);
147
- if (data) {
148
- const message = this.$formatMessage(event);
149
- this.adapter.emit('message.receive', message);
150
- this.logger.debug(
151
- `${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
152
- );
153
- }
154
- }
155
-
156
- async $sendMessage(options: SendOptions): Promise<string> {
157
- const content = Array.isArray(options.content) ? options.content : [options.content];
158
- const segments = content.map((c) =>
159
- typeof c === 'string' ? { type: 'text' as const, data: { text: c } } : (c as { type: string; data?: Record<string, unknown> }),
160
- );
161
- const message = toMilkyOutgoingSegments(segments);
162
- if (options.type === 'group') {
163
- const result = await callApi(this.apiOptions(), 'send_group_message', {
164
- group_id: parseInt(options.id, 10),
165
- message,
166
- });
167
- const seq = (result as { message_seq?: number }).message_seq;
168
- this.logger.debug(`${this.$config.name} send group(${options.id}):${segment.raw(options.content)}`);
169
- return seq != null ? `group:${options.id}:${seq}` : '';
170
- }
171
- if (options.type === 'private') {
172
- const result = await callApi(this.apiOptions(), 'send_private_message', {
173
- user_id: parseInt(options.id, 10),
174
- message,
175
- });
176
- const seq = (result as { message_seq?: number }).message_seq;
177
- this.logger.debug(`${this.$config.name} send private(${options.id}):${segment.raw(options.content)}`);
178
- return seq != null ? `friend:${options.id}:${seq}` : '';
179
- }
180
- throw new Error('Either group or private must be provided');
181
- }
182
-
183
- async $recallMessage(id: string): Promise<void> {
184
- const parsed = parseMilkyMessageId(id);
185
- if (!parsed) throw new Error(`Invalid message id: ${id}`);
186
- if (parsed.message_scene === 'group') {
187
- await callApi(this.apiOptions(), 'recall_group_message', {
188
- group_id: parsed.peer_id,
189
- message_seq: parsed.message_seq,
190
- });
191
- } else {
192
- await callApi(this.apiOptions(), 'recall_private_message', {
193
- user_id: parsed.peer_id,
194
- message_seq: parsed.message_seq,
195
- });
196
- }
197
- }
198
-
199
- async kickMember(groupId: number, userId: number, rejectAddRequest = false): Promise<boolean> {
200
- await callApi(this.apiOptions(), 'kick_group_member', {
201
- group_id: groupId,
202
- user_id: userId,
203
- reject_add_request: rejectAddRequest,
204
- });
205
- return true;
206
- }
207
-
208
- async muteMember(groupId: number, userId: number, duration = 600): Promise<boolean> {
209
- await callApi(this.apiOptions(), 'set_group_member_mute', {
210
- group_id: groupId,
211
- user_id: userId,
212
- duration,
213
- });
214
- return true;
215
- }
216
-
217
- async muteAll(groupId: number, enable = true): Promise<boolean> {
218
- await callApi(this.apiOptions(), 'set_group_whole_mute', { group_id: groupId, is_mute: enable });
219
- return true;
220
- }
221
-
222
- async setAdmin(groupId: number, userId: number, enable = true): Promise<boolean> {
223
- await callApi(this.apiOptions(), 'set_group_member_admin', {
224
- group_id: groupId,
225
- user_id: userId,
226
- is_set: enable,
227
- });
228
- return true;
229
- }
230
-
231
- async setCard(groupId: number, userId: number, card: string): Promise<boolean> {
232
- await callApi(this.apiOptions(), 'set_group_member_card', {
233
- group_id: groupId,
234
- user_id: userId,
235
- card,
236
- });
237
- return true;
238
- }
239
-
240
- async setTitle(groupId: number, userId: number, title: string): Promise<boolean> {
241
- await callApi(this.apiOptions(), 'set_group_member_special_title', {
242
- group_id: groupId,
243
- user_id: userId,
244
- special_title: title,
245
- });
246
- return true;
247
- }
248
-
249
- async setGroupName(groupId: number, name: string): Promise<boolean> {
250
- await callApi(this.apiOptions(), 'set_group_name', { group_id: groupId, new_group_name: name });
251
- return true;
252
- }
253
-
254
- async getMemberList(groupId: number): Promise<unknown[]> {
255
- return callApi(this.apiOptions(), 'get_group_member_list', { group_id: groupId }) as Promise<unknown[]>;
256
- }
257
-
258
- async getGroupInfo(groupId: number): Promise<unknown> {
259
- return callApi(this.apiOptions(), 'get_group_info', { group_id: groupId });
260
- }
261
-
262
- private startHeartbeat(): void {
263
- const interval = this.$config.heartbeat_interval ?? 30000;
264
- this.heartbeatTimer = setInterval(() => {
265
- if (this.#client?.readyState === WebSocket.OPEN) this.#client.ping();
266
- }, interval);
267
- }
268
- }
package/src/types.ts DELETED
@@ -1,73 +0,0 @@
1
- /**
2
- * Milky 适配器类型与配置(与官方文档一致)
3
- */
4
-
5
- /** 配置公共字段;单一适配器下 context 均为 'milky',连接方式由 connection 区分 */
6
- export interface MilkyConfigBase {
7
- context: 'milky';
8
- name: string;
9
- baseUrl: string;
10
- access_token?: string;
11
- }
12
-
13
- /** WebSocket 正向连接 */
14
- export interface MilkyWsConfig extends MilkyConfigBase {
15
- connection: 'ws';
16
- reconnect_interval?: number;
17
- heartbeat_interval?: number;
18
- }
19
-
20
- /** SSE 连接 */
21
- export interface MilkySseConfig extends MilkyConfigBase {
22
- connection: 'sse';
23
- }
24
-
25
- /** Webhook(协议端 POST 到应用) */
26
- export interface MilkyWebhookConfig extends MilkyConfigBase {
27
- connection: 'webhook';
28
- path: string;
29
- }
30
-
31
- /** WebSocket 反向(协议端连应用) */
32
- export interface MilkyWssConfig extends MilkyConfigBase {
33
- connection: 'wss';
34
- path: string;
35
- heartbeat_interval?: number;
36
- }
37
-
38
- export type MilkyBotConfig = MilkyWsConfig | MilkySseConfig | MilkyWebhookConfig | MilkyWssConfig;
39
-
40
- /** 协议端 API 响应:status、retcode、data?、message? */
41
- export interface MilkyApiResponse<T = unknown> {
42
- status: string;
43
- retcode: number;
44
- data?: T;
45
- message?: string;
46
- }
47
-
48
- /** 事件结构:event_type、time、self_id、data */
49
- export interface MilkyEvent {
50
- event_type: string;
51
- time: number;
52
- self_id: number;
53
- data?: Record<string, unknown>;
54
- }
55
-
56
- /** 接收消息 data(message_receive):message_scene、peer_id、message_seq、sender_id、time、segments、可选 friend/group/group_member */
57
- export interface MilkyIncomingMessage {
58
- message_scene: 'friend' | 'group' | 'temp';
59
- peer_id: number;
60
- message_seq: number;
61
- sender_id: number;
62
- time: number;
63
- segments: MilkyIncomingSegment[];
64
- friend?: { user_id: number; nickname?: string };
65
- group?: { group_id: number; group_name?: string };
66
- group_member?: { user_id: number; nickname?: string; card?: string; role?: string };
67
- }
68
-
69
- /** 接收消息段:type、data */
70
- export interface MilkyIncomingSegment {
71
- type: string;
72
- data?: Record<string, unknown>;
73
- }
package/src/utils.ts DELETED
@@ -1,174 +0,0 @@
1
- /**
2
- * Milky 事件/消息段与 zhin Message 的转换
3
- */
4
- import type { MessageBase, SendContent } from 'zhin.js';
5
- import { Message } from 'zhin.js';
6
- import type { MilkyEvent, MilkyIncomingMessage, MilkyIncomingSegment } from './types.js';
7
-
8
- /** 将 Milky 接收消息段转为 zhin 的 $content(MessageSegment[]) */
9
- export function formatMilkySegments(segments: MilkyIncomingSegment[]): Array<{ type: string; data: Record<string, unknown> }> {
10
- return segments.map((seg) => {
11
- const type = seg.type;
12
- const data = seg.data ?? {};
13
- switch (type) {
14
- case 'text':
15
- return { type: 'text', data: { text: (data as { text?: string }).text ?? '' } };
16
- case 'mention':
17
- return { type: 'at', data: { id: String((data as { user_id?: number }).user_id ?? '') } };
18
- case 'mention_all':
19
- return { type: 'at', data: { type: 'all' } };
20
- case 'face':
21
- return { type: 'face', data: { id: (data as { face_id?: string }).face_id ?? '' } };
22
- case 'reply':
23
- return {
24
- type: 'reply',
25
- data: { message_seq: (data as { message_seq?: number }).message_seq },
26
- };
27
- case 'image':
28
- return {
29
- type: 'image',
30
- data: {
31
- url: (data as { temp_url?: string }).temp_url ?? (data as { resource_id?: string }).resource_id ?? '',
32
- },
33
- };
34
- case 'record':
35
- return {
36
- type: 'record',
37
- data: {
38
- url: (data as { temp_url?: string }).temp_url ?? (data as { resource_id?: string }).resource_id ?? '',
39
- },
40
- };
41
- case 'video':
42
- return {
43
- type: 'video',
44
- data: {
45
- url: (data as { temp_url?: string }).temp_url ?? (data as { resource_id?: string }).resource_id ?? '',
46
- },
47
- };
48
- case 'file':
49
- return {
50
- type: 'file',
51
- data: {
52
- file_id: (data as { file_id?: string }).file_id,
53
- name: (data as { file_name?: string }).file_name,
54
- },
55
- };
56
- default:
57
- return { type, data: data as Record<string, unknown> };
58
- }
59
- });
60
- }
61
-
62
- /** message_receive 事件的 data 转为 zhin Message 的构造参数(含 $adapter、$bot) */
63
- export function formatMilkyMessagePayload(
64
- event: MilkyEvent,
65
- data: MilkyIncomingMessage,
66
- recallFn: (msgId: string) => Promise<void>,
67
- replyFn: (channel: { id: string; type: 'group' | 'private' }, content: (string | { type: string; data?: Record<string, unknown> })[], quote?: boolean | string) => Promise<string>,
68
- adapterName: 'milky',
69
- botName: string,
70
- ): MessageBase {
71
- const scene = data.message_scene;
72
- const isGroup = scene === 'group';
73
- const channelId = data.peer_id.toString();
74
- const msgId = `${data.message_scene}:${data.peer_id}:${data.message_seq}`;
75
- const channel = { id: channelId, type: (isGroup ? 'group' : 'private') as 'group' | 'private' };
76
- const senderId = data.sender_id.toString();
77
- const senderName =
78
- (isGroup ? data.group_member?.card ?? data.group_member?.nickname : data.friend?.nickname) ?? senderId;
79
- const content = formatMilkySegments(data.segments);
80
- const raw = content.map((c) => (c.type === 'text' ? (c.data as { text?: string }).text : '')).join('');
81
-
82
- return {
83
- $id: msgId,
84
- $adapter: 'milky',
85
- $bot: botName,
86
- $channel: channel,
87
- $sender: { id: senderId, name: senderName },
88
- $content: content,
89
- $raw: raw,
90
- $timestamp: data.time,
91
- $recall: () => recallFn(msgId),
92
- $reply: (cnt: SendContent, quote?: boolean | string) =>
93
- replyFn(channel, (Array.isArray(cnt) ? cnt : [cnt]) as (string | { type: string; data?: Record<string, unknown> })[], quote),
94
- };
95
- }
96
-
97
- /** 根据 event_type 判断是否为 message_receive,并解析 data 为 MilkyIncomingMessage */
98
- export function parseMessageReceiveData(event: MilkyEvent): MilkyIncomingMessage | null {
99
- if (event.event_type !== 'message_receive' || !event.data) return null;
100
- const data = event.data as unknown as MilkyIncomingMessage;
101
- if (!data.message_scene || !Number.isInteger(data.peer_id) || !Array.isArray(data.segments)) return null;
102
- return data;
103
- }
104
-
105
- /** zhin SendContent(string | MessageElement 或数组)转为 Milky OutgoingSegment[](发送用) */
106
- export function toMilkyOutgoingSegments(
107
- content: (string | { type: string; data?: Record<string, unknown> })[],
108
- ): Array<{ type: string; data: Record<string, unknown> }> {
109
- const out: Array<{ type: string; data: Record<string, unknown> }> = [];
110
- for (const seg of content) {
111
- const type = typeof seg === 'string' ? 'text' : seg.type;
112
- const data = typeof seg === 'string' ? { text: seg } : (seg.data ?? {});
113
- switch (type) {
114
- case 'text':
115
- out.push({ type: 'text', data: { text: String((data as { text?: string }).text ?? '') } });
116
- break;
117
- case 'at':
118
- if ((data as { type?: string }).type === 'all') {
119
- out.push({ type: 'mention_all', data: {} });
120
- } else {
121
- const id = (data as { id?: string }).id;
122
- if (id) out.push({ type: 'mention', data: { user_id: Number(id) || 0 } });
123
- }
124
- break;
125
- case 'face':
126
- out.push({ type: 'face', data: { face_id: String((data as { id?: string }).id ?? ''), is_large: false } });
127
- break;
128
- case 'reply':
129
- // message_id 可能为 scene:peer:seq 或仅 message_seq
130
- const mid = (data as { message_id?: string; message_seq?: number }).message_id ?? (data as { message_seq?: number }).message_seq;
131
- const seq = typeof mid === 'number' ? mid : parseInt(String(mid).split(':').pop() ?? '0', 10);
132
- out.push({ type: 'reply', data: { message_seq: seq } });
133
- break;
134
- case 'image':
135
- out.push({
136
- type: 'image',
137
- data: { uri: String((data as { url?: string }).url ?? (data as { uri?: string }).uri ?? '') },
138
- });
139
- break;
140
- case 'record':
141
- out.push({
142
- type: 'record',
143
- data: { uri: String((data as { url?: string }).url ?? (data as { uri?: string }).uri ?? '') },
144
- });
145
- break;
146
- case 'video':
147
- out.push({
148
- type: 'video',
149
- data: {
150
- uri: String((data as { url?: string }).url ?? (data as { uri?: string }).uri ?? ''),
151
- thumb_uri: (data as { thumb_uri?: string }).thumb_uri,
152
- },
153
- });
154
- break;
155
- default:
156
- out.push({ type, data: data as Record<string, unknown> });
157
- }
158
- }
159
- return out;
160
- }
161
-
162
- /** 解析 $id 为 message_scene、peer_id、message_seq(用于撤回) */
163
- export function parseMilkyMessageId(
164
- msgId: string,
165
- ): { message_scene: 'friend' | 'group' | 'temp'; peer_id: number; message_seq: number } | null {
166
- const parts = msgId.split(':');
167
- if (parts.length < 3) return null;
168
- const [scene, peer, seq] = parts;
169
- if (scene !== 'friend' && scene !== 'group' && scene !== 'temp') return null;
170
- const peerId = parseInt(peer, 10);
171
- const messageSeq = parseInt(seq, 10);
172
- if (Number.isNaN(peerId) || Number.isNaN(messageSeq)) return null;
173
- return { message_scene: scene, peer_id: peerId, message_seq: messageSeq };
174
- }