@napuketto/adapter 0.5.0 → 0.6.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.
@@ -1,6 +1,8 @@
1
1
  import { o as ActionRegistry, r as BaseProtocolAdapter } from "../core-DyXyDUcU.mjs";
2
+ import { n as firstUntranslatedGrayTip, t as collectGrayTipUids } from "../gray-tip-LwBee5mo.mjs";
3
+ import { n as MS_TO_SEC$2, r as resolveUin, t as narrowBuddyReqs } from "../buddy-req-Q7Q4bVuz.mjs";
2
4
  import { n as subscribeRawMessages } from "../raw-message-D_ecJy4C.mjs";
3
- import { ChatType, GroupApi, isKernelError, kernelError, toCanonicalElements } from "@napuketto/kernel";
5
+ import { ChatType, GrayTipSubType, GroupApi, GroupNotifyMsgStatus, GroupNotifyMsgType, NTGroupRequestOperateTypes, TicketApi, TipGroupElementType, isKernelError, kernelError, toCanonicalElements } from "@napuketto/kernel";
4
6
  import { mkdir, rename, writeFile } from "node:fs/promises";
5
7
  import { detectFileType, downloadUrl, inferExtension } from "@napuketto/media";
6
8
  import { z } from "zod";
@@ -9,6 +11,124 @@ import { join } from "node:path";
9
11
  import { HttpClient, HttpServer, createSseResponse } from "@napuketto/network";
10
12
  import { WebSocket } from "ws";
11
13
  import { fileURLToPath } from "node:url";
14
+ //#region src/milky/api.ts
15
+ var MilkyApi = class {
16
+ msgApi;
17
+ groupApi;
18
+ friendApi;
19
+ profileApi;
20
+ profileLikeApi;
21
+ ticketApi;
22
+ webApi;
23
+ groupNotifyApi;
24
+ richMediaApi;
25
+ self;
26
+ groupCache;
27
+ qqVersion;
28
+ implVersion;
29
+ mediaDir;
30
+ /** message_seq ↔ msgId 双向映射(接收链路 record,动作侧反查)。 */
31
+ seqIndex;
32
+ constructor(opts, seqIndex) {
33
+ this.msgApi = opts.msgApi;
34
+ this.groupApi = opts.groupApi;
35
+ this.friendApi = opts.friendApi;
36
+ this.profileApi = opts.profileApi;
37
+ this.profileLikeApi = opts.profileLikeApi;
38
+ this.ticketApi = opts.ticketApi;
39
+ this.webApi = opts.webApi;
40
+ this.groupNotifyApi = opts.groupNotifyApi;
41
+ this.richMediaApi = opts.richMediaApi;
42
+ this.self = opts.self;
43
+ this.groupCache = opts.groupCache;
44
+ this.qqVersion = opts.qqVersion;
45
+ this.implVersion = opts.implVersion;
46
+ this.mediaDir = opts.mediaDir;
47
+ this.seqIndex = seqIndex;
48
+ }
49
+ /** uin → uid(私聊目标 / at 段发送转换 / 群管动作的 uid 入参)。 */
50
+ uinToUid(uins) {
51
+ return this.groupApi.uinToUid(uins);
52
+ }
53
+ /** uid → uin(事件路径 mention 转换)。 */
54
+ uidToUin(uids) {
55
+ return this.groupApi.uidToUin(uids);
56
+ }
57
+ /**
58
+ * uin(数字)→ uid(群管动作入参转换);转换失败抛 INVALID_PARAM
59
+ * (Milky 入参 user_id 都是 uin,kernel 群管 API 要 uid)。
60
+ */
61
+ async resolveUidFromUin(uin) {
62
+ const uinStr = String(uin);
63
+ try {
64
+ const uid = (await this.uinToUid([uinStr])).get(uinStr);
65
+ if (uid !== void 0 && uid !== "") return uid;
66
+ } catch {}
67
+ throw kernelError(`用户 ${uinStr} 不存在或 uin→uid 转换失败`, "INVALID_PARAM");
68
+ }
69
+ /**
70
+ * message_scene + 数字 ID → NT Peer。
71
+ * group = 群号直作 peerUid;friend/temp = uin → uid(失败退 uin 原样,交 QQ 端判断)。
72
+ */
73
+ async resolvePeer(scene, id) {
74
+ if (scene === "group") return {
75
+ chatType: ChatType.GROUP,
76
+ peerUid: String(id)
77
+ };
78
+ const uin = String(id);
79
+ let uid;
80
+ try {
81
+ uid = (await this.uinToUid([uin])).get(uin);
82
+ } catch {}
83
+ return {
84
+ chatType: scene === "friend" ? ChatType.C2C : ChatType.C2C_TEMP,
85
+ peerUid: uid ?? uin
86
+ };
87
+ }
88
+ /**
89
+ * seq → msgId(索引优先,未登记扫描近段历史兜底)。
90
+ * 两层都未命中返回 undefined(调用方抛 NOT_FOUND)。
91
+ */
92
+ async resolveMsgIdBySeq(peer, seq, chatType) {
93
+ const indexed = this.seqIndex.resolveMsgId(chatType, peer.peerUid, seq);
94
+ if (indexed !== void 0) return indexed;
95
+ const hit = (await this.msgApi.fetchMessages(peer, { count: 60 }).catch(() => [])).find((m) => Number(m.msgSeq) === seq);
96
+ if (hit === void 0) return;
97
+ this.seqIndex.record(hit);
98
+ return hit.msgId;
99
+ }
100
+ /** 发送结果回查:msgId → {message_seq, time}(sendMessage 只回 msgId)。 */
101
+ async resolveSentOutput(peer, msgId) {
102
+ const [sent] = await this.msgApi.fetchMsgsByMsgId(peer, [msgId]);
103
+ if (sent === void 0) throw kernelError("发送结果回查失败(msgId 未命中)", "UNKNOWN");
104
+ this.seqIndex.record(sent);
105
+ return {
106
+ message_seq: Number(sent.msgSeq),
107
+ time: Math.floor(Number(sent.msgTime) / 1e3)
108
+ };
109
+ }
110
+ };
111
+ /** 通知查找默认搜索数量(单屏全量,OB11 DEFAULT_COUNT 同值)。 */
112
+ const NOTIFY_SEARCH_COUNT$1 = 100;
113
+ /**
114
+ * 按通知 seq 查找群通知(先非可疑后可疑,OB11 set_group_add_request 同路径)。
115
+ * 未找到抛 NOT_FOUND(应答动作的 seq 无效或已过期)。
116
+ * 模块级函数(非类成员):应答动作的公共入口,OB11 同路径。
117
+ */
118
+ async function findGroupNotify(api, seq) {
119
+ const hit = (await api.groupNotifyApi.getSingleScreenNotifies(false, NOTIFY_SEARCH_COUNT$1)).find((item) => item.seq === seq);
120
+ if (hit !== void 0) return hit;
121
+ const doubtHit = (await api.groupNotifyApi.getSingleScreenNotifies(true, NOTIFY_SEARCH_COUNT$1)).find((item) => item.seq === seq);
122
+ if (doubtHit !== void 0) return doubtHit;
123
+ throw kernelError(`未找到通知 ${seq}(无效或已过期)`, "NOT_FOUND");
124
+ }
125
+ /** 按发起者 uid 查找好友申请(getBuddyReqList 匹配 friendUid);未找到抛 NOT_FOUND。 */
126
+ async function findBuddyReq(api, initiatorUid) {
127
+ const hit = (await api.friendApi.getBuddyReqList()).find((req) => req.friendUid === initiatorUid);
128
+ if (hit === void 0) throw kernelError(`未找到来自 ${initiatorUid} 的好友申请`, "NOT_FOUND");
129
+ return hit;
130
+ }
131
+ //#endregion
12
132
  //#region src/milky/helper/entities.ts
13
133
  /** GroupDetailInfo / Group → GroupEntity(detail 缺失时全兜底)。 */
14
134
  function toGroupEntity(detail, groupCode) {
@@ -70,6 +190,706 @@ function toFriendEntity(friend, uin, nick) {
70
190
  };
71
191
  }
72
192
  //#endregion
193
+ //#region src/milky/helper/request.ts
194
+ /** uid → uin(core 共享解析;ctx 携带映射)。 */
195
+ function toUin$1(uid, ctx) {
196
+ return resolveUin(uid, ctx.uidToUin);
197
+ }
198
+ /** BuddyReq → friend_request 事件(comment 取 words 字段,形状待校准,缺省空串)。 */
199
+ function toFriendRequestEvent(req, ctx) {
200
+ const words = req["words"];
201
+ return {
202
+ time: Math.floor(Date.now() / MS_TO_SEC$2),
203
+ self_id: Number(ctx.selfUin),
204
+ event_type: "friend_request",
205
+ data: {
206
+ initiator_id: toUin$1(req.friendUid, ctx),
207
+ initiator_uid: req.friendUid,
208
+ comment: typeof words === "string" ? words : "",
209
+ via: req.sourceName ?? ""
210
+ }
211
+ };
212
+ }
213
+ /** GroupNotify → Milky 群请求事件(非未处理状态 / 非请求类返回 null)。 */
214
+ function toGroupRequestEvent(notify, doubt, ctx) {
215
+ if (notify.status !== GroupNotifyMsgStatus.KUNHANDLE) return null;
216
+ const groupId = Number(notify.group?.groupCode ?? 0);
217
+ const seq = Number(notify.seq);
218
+ const time = Math.floor(Date.now() / MS_TO_SEC$2);
219
+ const selfId = Number(ctx.selfUin);
220
+ switch (notify.type) {
221
+ case GroupNotifyMsgType.REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS: return {
222
+ time,
223
+ self_id: selfId,
224
+ event_type: "group_join_request",
225
+ data: {
226
+ group_id: groupId,
227
+ notification_seq: seq,
228
+ is_filtered: doubt,
229
+ initiator_id: toUin$1(notify.user1?.uid, ctx),
230
+ comment: notify.postscript ?? ""
231
+ }
232
+ };
233
+ case GroupNotifyMsgType.INVITED_NEED_ADMINI_STRATOR_PASS: return {
234
+ time,
235
+ self_id: selfId,
236
+ event_type: "group_invited_join_request",
237
+ data: {
238
+ group_id: groupId,
239
+ notification_seq: seq,
240
+ initiator_id: toUin$1(notify.user1?.uid, ctx),
241
+ target_user_id: toUin$1(notify.user2?.uid, ctx)
242
+ }
243
+ };
244
+ case GroupNotifyMsgType.INVITED_BY_MEMBER: return {
245
+ time,
246
+ self_id: selfId,
247
+ event_type: "group_invitation",
248
+ data: {
249
+ group_id: groupId,
250
+ invitation_seq: seq,
251
+ initiator_id: toUin$1(notify.user1?.uid, ctx),
252
+ source_group_id: null
253
+ }
254
+ };
255
+ default: return null;
256
+ }
257
+ }
258
+ /**
259
+ * 群通知涉及的 uid 集合(去重;get_group_notifications 与事件广播共用)。
260
+ * kernel GroupEventChannel 的 onGroupNotifiesUpdated 回调参数已类型化
261
+ * (GroupNotify[]),无需防御收窄。
262
+ */
263
+ function collectNotifyUids(notifies) {
264
+ const uids = /* @__PURE__ */ new Set();
265
+ for (const n of notifies) for (const uid of [n.user1?.uid, n.user2?.uid]) if (uid !== void 0 && uid !== "") uids.add(uid);
266
+ return [...uids];
267
+ }
268
+ //#endregion
269
+ //#region src/milky/action/error-map.ts
270
+ const milkyErrorCodeMap = {
271
+ SEND_FAILED: -500,
272
+ PERMISSION_DENIED: -405,
273
+ NOT_FOUND: -404,
274
+ TIMEOUT: -504,
275
+ NOT_LOGIN: -403,
276
+ INVALID_PARAM: -400,
277
+ INVALID_STATE: -409,
278
+ UNKNOWN: -500
279
+ };
280
+ //#endregion
281
+ //#region src/milky/action/base-action.ts
282
+ /**
283
+ * MilkyAction:Milky 动作基类(zod 校验 + KernelError → 负数 retcode)
284
+ *
285
+ * 与 OB11 BaseAction 分离的原因:Milky 响应信封无 echo、参数解析失败恒
286
+ * retcode=-400(规范),不复用 OB11 的 400/1400 语义。
287
+ * 响应信封由 transport 组装(status/retcode/data/message)。
288
+ */
289
+ /** zod 错误摘要(单行,issues 取前 3 条)。 */
290
+ function zodSummary(error) {
291
+ const parts = [];
292
+ for (const issue of error.issues.slice(0, 3)) parts.push(`${issue.path.map(String).join(".")}: ${issue.message}`);
293
+ return parts.join("; ");
294
+ }
295
+ var MilkyAction = class {
296
+ /** 校验并执行(HTTP /api 入口;WS /event 不承载动作)。 */
297
+ async handle(payload) {
298
+ const parsed = this.schema.safeParse(payload);
299
+ if (!parsed.success) return {
300
+ status: "failed",
301
+ retcode: -400,
302
+ data: null,
303
+ message: `参数解析失败: ${zodSummary(parsed.error)}`
304
+ };
305
+ try {
306
+ return {
307
+ status: "ok",
308
+ retcode: 0,
309
+ data: await this._handle(parsed.data),
310
+ message: ""
311
+ };
312
+ } catch (err) {
313
+ if (isKernelError(err)) return {
314
+ status: "failed",
315
+ retcode: this.errorCodeMap[err.code] ?? this.errorCodeMap.UNKNOWN,
316
+ data: null,
317
+ message: err.message
318
+ };
319
+ return {
320
+ status: "failed",
321
+ retcode: this.errorCodeMap.UNKNOWN,
322
+ data: null,
323
+ message: err instanceof Error ? err.message : "未知错误"
324
+ };
325
+ }
326
+ }
327
+ };
328
+ /**
329
+ * 持 MilkyApi 的动作基类:api 注入 + 统一错误映射表(milkyErrorCodeMap)收拢,
330
+ * 动作子类只写 name / schema / _handle。
331
+ */
332
+ var MilkyApiAction = class extends MilkyAction {
333
+ errorCodeMap = milkyErrorCodeMap;
334
+ api;
335
+ constructor(api) {
336
+ super();
337
+ this.api = api;
338
+ }
339
+ };
340
+ //#endregion
341
+ //#region src/milky/action/group.ts
342
+ const groupIdField$1 = { group_id: z.number().int().min(10001) };
343
+ const userIdField = { user_id: z.number().int().min(10001) };
344
+ const optBool = z.boolean().nullable().optional();
345
+ const optStr$1 = z.string().nullable().optional();
346
+ /** get_group_info:获取群信息(GroupCache 详情缓存优先)。 */
347
+ var GetGroupInfoAction = class extends MilkyApiAction {
348
+ name = "get_group_info";
349
+ schema = z.object({
350
+ ...groupIdField$1,
351
+ no_cache: optBool
352
+ });
353
+ async _handle(payload) {
354
+ const groupCode = String(payload.group_id);
355
+ return { group: toGroupEntity(this.api.groupCache !== void 0 ? await this.api.groupCache.getGroupDetail(groupCode) : await this.api.groupApi.getGroupInfo(groupCode), groupCode) };
356
+ }
357
+ };
358
+ /** get_group_member_list:获取群成员列表(GroupCache 惰性回填)。 */
359
+ var GetGroupMemberListAction = class extends MilkyApiAction {
360
+ name = "get_group_member_list";
361
+ schema = z.object({
362
+ ...groupIdField$1,
363
+ no_cache: optBool
364
+ });
365
+ async _handle(payload) {
366
+ return { members: (await membersOf(this.api, String(payload.group_id))).map((m) => toGroupMemberEntity(m, String(payload.group_id), m.uin)) };
367
+ }
368
+ };
369
+ /** get_group_member_info:获取群成员信息(user_id = uin → 成员单查)。 */
370
+ var GetGroupMemberInfoAction = class extends MilkyApiAction {
371
+ name = "get_group_member_info";
372
+ schema = z.object({
373
+ ...groupIdField$1,
374
+ ...userIdField,
375
+ no_cache: optBool
376
+ });
377
+ async _handle(payload) {
378
+ return { member: toGroupMemberEntity(await memberOf(this.api, payload.group_id, payload.user_id), String(payload.group_id), String(payload.user_id)) };
379
+ }
380
+ };
381
+ /** set_group_name:设置群名。 */
382
+ var SetGroupNameAction = class extends MilkyApiAction {
383
+ name = "set_group_name";
384
+ schema = z.object({
385
+ ...groupIdField$1,
386
+ new_group_name: z.string()
387
+ });
388
+ async _handle(payload) {
389
+ await this.api.groupApi.modifyGroupName(String(payload.group_id), payload.new_group_name);
390
+ return {};
391
+ }
392
+ };
393
+ /** set_group_member_card:设置群名片。 */
394
+ var SetGroupMemberCardAction = class extends MilkyApiAction {
395
+ name = "set_group_member_card";
396
+ schema = z.object({
397
+ ...groupIdField$1,
398
+ ...userIdField,
399
+ card: z.string()
400
+ });
401
+ async _handle(payload) {
402
+ const uid = await this.api.resolveUidFromUin(payload.user_id);
403
+ this.api.groupApi.setMemberCardName(String(payload.group_id), uid, payload.card);
404
+ return {};
405
+ }
406
+ };
407
+ /** set_group_member_admin:设置/取消管理员(is_set 缺省 true)。 */
408
+ var SetGroupMemberAdminAction = class extends MilkyApiAction {
409
+ name = "set_group_member_admin";
410
+ schema = z.object({
411
+ ...groupIdField$1,
412
+ ...userIdField,
413
+ is_set: optBool
414
+ });
415
+ async _handle(payload) {
416
+ const uid = await this.api.resolveUidFromUin(payload.user_id);
417
+ this.api.groupApi.setMemberRole(String(payload.group_id), uid, payload.is_set === false ? NTGroupMemberRoleMember : NTGroupMemberRoleAdmin);
418
+ return {};
419
+ }
420
+ };
421
+ /** set_group_member_mute:禁言成员(duration 秒,0 解除)。 */
422
+ var SetGroupMemberMuteAction = class extends MilkyApiAction {
423
+ name = "set_group_member_mute";
424
+ schema = z.object({
425
+ ...groupIdField$1,
426
+ ...userIdField,
427
+ duration: z.number().int().min(0).nullable().optional()
428
+ });
429
+ async _handle(payload) {
430
+ const uid = await this.api.resolveUidFromUin(payload.user_id);
431
+ await this.api.groupApi.setMemberShutUp(String(payload.group_id), [{
432
+ uid,
433
+ duration: payload.duration ?? 0
434
+ }]);
435
+ return {};
436
+ }
437
+ };
438
+ /** set_group_whole_mute:全员禁言开关(is_mute 缺省 true)。 */
439
+ var SetGroupWholeMuteAction = class extends MilkyApiAction {
440
+ name = "set_group_whole_mute";
441
+ schema = z.object({
442
+ ...groupIdField$1,
443
+ is_mute: optBool
444
+ });
445
+ async _handle(payload) {
446
+ await this.api.groupApi.setGroupShutUp(String(payload.group_id), payload.is_mute !== false);
447
+ return {};
448
+ }
449
+ };
450
+ /** kick_group_member:移除成员(reject_add_request 默认 false)。 */
451
+ var KickGroupMemberAction = class extends MilkyApiAction {
452
+ name = "kick_group_member";
453
+ schema = z.object({
454
+ ...groupIdField$1,
455
+ ...userIdField,
456
+ reject_add_request: optBool
457
+ });
458
+ async _handle(payload) {
459
+ const uid = await this.api.resolveUidFromUin(payload.user_id);
460
+ await this.api.groupApi.kickMember(String(payload.group_id), [uid], payload.reject_add_request === true);
461
+ return {};
462
+ }
463
+ };
464
+ /** quit_group:退出群聊。 */
465
+ var QuitGroupAction = class extends MilkyApiAction {
466
+ name = "quit_group";
467
+ schema = z.object({ ...groupIdField$1 });
468
+ async _handle(payload) {
469
+ await this.api.groupApi.quitGroup(String(payload.group_id));
470
+ return {};
471
+ }
472
+ };
473
+ /**
474
+ * get_group_essence_messages:获取精华消息列表。
475
+ * WebApi.getEssenceMsgList 全量拉取后按 page_index/page_size 客户端分页
476
+ * (kernel 无分页参数);message_time 无源取 operation_time 兜底。
477
+ */
478
+ var GetGroupEssenceMessagesAction = class extends MilkyApiAction {
479
+ name = "get_group_essence_messages";
480
+ schema = z.object({
481
+ ...groupIdField$1,
482
+ page_index: z.number().int().min(0),
483
+ page_size: z.number().int().min(0)
484
+ });
485
+ async _handle(payload) {
486
+ const items = await this.api.webApi.getEssenceMsgList(String(payload.group_id));
487
+ const start = payload.page_index * payload.page_size;
488
+ const page = items.slice(start, start + payload.page_size);
489
+ return {
490
+ messages: page.map((item) => essenceToEntity(item, payload.group_id)),
491
+ is_end: start + page.length >= items.length
492
+ };
493
+ }
494
+ };
495
+ /** set_group_essence_message:设置/取消精华(seq → msgId 反查)。 */
496
+ var SetGroupEssenceMessageAction = class extends MilkyApiAction {
497
+ name = "set_group_essence_message";
498
+ schema = z.object({
499
+ ...groupIdField$1,
500
+ message_seq: z.number().int().min(0),
501
+ is_set: optBool
502
+ });
503
+ async _handle(payload) {
504
+ const groupCode = String(payload.group_id);
505
+ const peer = {
506
+ chatType: ChatType.GROUP,
507
+ peerUid: groupCode
508
+ };
509
+ const msgId = await this.api.resolveMsgIdBySeq(peer, payload.message_seq, ChatType.GROUP);
510
+ if (msgId === void 0) throw kernelError(`消息 ${payload.message_seq} 不存在`, "NOT_FOUND");
511
+ if (payload.is_set === false) await this.api.groupApi.removeGroupEssence(groupCode, msgId);
512
+ else await this.api.groupApi.addGroupEssence(groupCode, msgId);
513
+ return {};
514
+ }
515
+ };
516
+ /** send_group_message_reaction:消息表情表态(reaction_type face→"1" / emoji→"2")。 */
517
+ var SendGroupMessageReactionAction = class extends MilkyApiAction {
518
+ name = "send_group_message_reaction";
519
+ schema = z.object({
520
+ ...groupIdField$1,
521
+ message_seq: z.number().int().min(0),
522
+ reaction: z.string().min(1),
523
+ reaction_type: z.enum(["face", "emoji"]).nullable().optional(),
524
+ is_add: z.boolean().nullable().optional()
525
+ });
526
+ async _handle(payload) {
527
+ const peer = {
528
+ chatType: ChatType.GROUP,
529
+ peerUid: String(payload.group_id)
530
+ };
531
+ await this.api.msgApi.setMsgEmojiLike(peer, {
532
+ msgSeq: String(payload.message_seq),
533
+ emojiId: payload.reaction,
534
+ emojiType: payload.reaction_type === "emoji" ? "2" : "1",
535
+ like: payload.is_add !== false
536
+ });
537
+ return {};
538
+ }
539
+ };
540
+ /**
541
+ * get_group_notifications:获取群通知列表。
542
+ * kernel getSingleScreenNotifies 为单屏快照(无翻页)——start_notification_seq
543
+ * 作客户端过滤(取 seq 更小的记录);next = 本页最小 seq(仍有余量时)。
544
+ */
545
+ var GetGroupNotificationsAction = class extends MilkyApiAction {
546
+ name = "get_group_notifications";
547
+ schema = z.object({
548
+ start_notification_seq: z.number().int().min(0).nullable().optional(),
549
+ is_filtered: optBool,
550
+ limit: z.number().int().min(0).nullable().optional()
551
+ });
552
+ async _handle(payload) {
553
+ const notifies = await this.api.groupNotifyApi.getSingleScreenNotifies(payload.is_filtered === true, NOTIFY_SEARCH_COUNT);
554
+ const start = payload.start_notification_seq;
555
+ const filtered = notifies.filter((n) => start === void 0 || start === null || Number(n.seq) < start);
556
+ const uidToUin = await this.collectUidToUin(filtered);
557
+ const entities = filtered.map((n) => notifyToEntity(n, payload.is_filtered === true, uidToUin)).filter((e) => e !== null);
558
+ const limit = payload.limit ?? NOTIFICATIONS_DEFAULT_LIMIT;
559
+ const page = entities.slice(0, limit);
560
+ const minSeq = page.reduce((acc, e) => acc === null ? e.notification_seq : Math.min(acc, e.notification_seq), null);
561
+ return {
562
+ notifications: page,
563
+ next_notification_seq: entities.length > page.length && minSeq !== null ? minSeq : null
564
+ };
565
+ }
566
+ /** 通知 user1/user2 uid → uin 批量转换(失败退空 Map,实体 id 兜底 0)。 */
567
+ async collectUidToUin(notifies) {
568
+ const uids = collectNotifyUids(notifies);
569
+ if (uids.length === 0) return /* @__PURE__ */ new Map();
570
+ return await this.api.uidToUin(uids).catch(() => /* @__PURE__ */ new Map());
571
+ }
572
+ };
573
+ /** accept/reject 群请求 / 群邀请公共基类:seq → 通知匹配 → handleGroupRequest。 */
574
+ var HandleGroupNotifyBase = class extends MilkyApiAction {
575
+ async respond(seq, doubt) {
576
+ const notify = await findGroupNotify(this.api, String(seq));
577
+ await this.api.groupNotifyApi.handleGroupRequest(doubt, notify, this.approve ? NTGroupRequestOperateTypes.KAGREE : NTGroupRequestOperateTypes.KREFUSE, " ");
578
+ return {};
579
+ }
580
+ };
581
+ /** accept_group_request:同意入群请求(notification_type/group_id 仅 schema 校验)。 */
582
+ var AcceptGroupRequestAction = class extends HandleGroupNotifyBase {
583
+ name = "accept_group_request";
584
+ schema = z.object({
585
+ notification_seq: z.number().int().min(0),
586
+ notification_type: z.enum(["join_request", "invited_join_request"]),
587
+ ...groupIdField$1,
588
+ is_filtered: optBool
589
+ });
590
+ approve = true;
591
+ async _handle(payload) {
592
+ return await this.respond(payload.notification_seq, payload.is_filtered === true);
593
+ }
594
+ };
595
+ /** reject_group_request:拒绝入群请求(reason 无源,kernel postscript 占位空格)。 */
596
+ var RejectGroupRequestAction = class extends HandleGroupNotifyBase {
597
+ name = "reject_group_request";
598
+ schema = z.object({
599
+ notification_seq: z.number().int().min(0),
600
+ notification_type: z.enum(["join_request", "invited_join_request"]),
601
+ ...groupIdField$1,
602
+ is_filtered: optBool,
603
+ reason: optStr$1
604
+ });
605
+ approve = false;
606
+ async _handle(payload) {
607
+ return await this.respond(payload.notification_seq, payload.is_filtered === true);
608
+ }
609
+ };
610
+ /** accept_group_invitation:接受入群邀请(按 invitation_seq 匹配)。 */
611
+ var AcceptGroupInvitationAction = class extends HandleGroupNotifyBase {
612
+ name = "accept_group_invitation";
613
+ schema = z.object({
614
+ ...groupIdField$1,
615
+ invitation_seq: z.number().int().min(0)
616
+ });
617
+ approve = true;
618
+ async _handle(payload) {
619
+ return await this.respond(payload.invitation_seq, false);
620
+ }
621
+ };
622
+ /** reject_group_invitation:拒绝入群邀请。 */
623
+ var RejectGroupInvitationAction = class extends HandleGroupNotifyBase {
624
+ name = "reject_group_invitation";
625
+ schema = z.object({
626
+ ...groupIdField$1,
627
+ invitation_seq: z.number().int().min(0)
628
+ });
629
+ approve = false;
630
+ async _handle(payload) {
631
+ return await this.respond(payload.invitation_seq, false);
632
+ }
633
+ };
634
+ /** ---------- 内部辅助 ---------- */
635
+ /** 通知查找数量(单屏全量,OB11 DEFAULT_COUNT 同值)。 */
636
+ const NOTIFY_SEARCH_COUNT = 100;
637
+ /** get_group_notifications 默认 limit(规范 default 20)。 */
638
+ const NOTIFICATIONS_DEFAULT_LIMIT = 20;
639
+ /** NTGroupMemberRole 数值(ADMIN=3 / MEMBER=2,setMemberRole 入参)。 */
640
+ const NTGroupMemberRoleAdmin = 3;
641
+ const NTGroupMemberRoleMember = 2;
642
+ /** 成员列表(GroupCache 优先,未装配直查)。 */
643
+ async function membersOf(api, groupCode) {
644
+ if (api.groupCache !== void 0) return await api.groupCache.getMembers(groupCode);
645
+ return await api.groupApi.getGroupMemberList(groupCode, true);
646
+ }
647
+ /** 成员单查(user_id = uin;缓存路径经 uid,直查路径按 uin 匹配)。 */
648
+ async function memberOf(api, groupId, userId) {
649
+ const groupCode = String(groupId);
650
+ const uin = String(userId);
651
+ if (api.groupCache !== void 0) {
652
+ const uid = await api.resolveUidFromUin(userId).catch(() => uin);
653
+ const member = await api.groupCache.getMember(groupCode, uid);
654
+ if (member === void 0) throw kernelError(`成员 ${uin} 不在群 ${groupCode}`, "NOT_FOUND");
655
+ return member;
656
+ }
657
+ const hit = (await api.groupApi.getGroupMemberList(groupCode, true)).find((m) => m.uin === uin);
658
+ if (hit === void 0) throw kernelError(`成员 ${uin} 不在群 ${groupCode}`, "NOT_FOUND");
659
+ return hit;
660
+ }
661
+ /** 精华项 → GroupEssenceMessage(msg_content 近似翻译:text / image)。 */
662
+ function essenceToEntity(item, groupId) {
663
+ const segments = [];
664
+ for (const c of item.msg_content) if (c.text !== void 0 && c.text !== "") segments.push({
665
+ type: "text",
666
+ data: { text: c.text }
667
+ });
668
+ else if (c.image_url !== void 0 && c.image_url !== "") segments.push({
669
+ type: "image",
670
+ data: {
671
+ resource_id: c.image_url,
672
+ temp_url: c.image_url,
673
+ width: 0,
674
+ height: 0,
675
+ summary: "",
676
+ sub_type: "normal"
677
+ }
678
+ });
679
+ return {
680
+ group_id: groupId,
681
+ message_seq: item.msg_seq,
682
+ message_time: item.add_digest_time,
683
+ sender_id: Number(item.sender_uin) || 0,
684
+ sender_name: item.sender_nick,
685
+ operator_id: Number(item.add_digest_uin) || 0,
686
+ operator_name: item.add_digest_nick,
687
+ operation_time: item.add_digest_time,
688
+ segments
689
+ };
690
+ }
691
+ /**
692
+ * GroupNotify → GroupNotificationEntity(uidToUin 已由调用方批量转换)。
693
+ * 类型→构造器表:7=入群申请 / 5=成员邀请他人 / 8=设管理 / 12,13=取消管理 /
694
+ * 9,10=踢出 / 11=退群;1=邀请自身无实体变体(事件层表达),其余 null。
695
+ */
696
+ const NOTIFY_ENTITY_BUILDERS = {
697
+ 7: (n, uinOf) => ({
698
+ type: "join_request",
699
+ group_id: Number(n.group?.groupCode ?? 0),
700
+ notification_seq: Number(n.seq),
701
+ is_filtered: false,
702
+ initiator_id: uinOf(n.user1?.uid),
703
+ state: "pending",
704
+ operator_id: null,
705
+ comment: n.postscript ?? ""
706
+ }),
707
+ 5: (n, uinOf) => ({
708
+ type: "invited_join_request",
709
+ group_id: Number(n.group?.groupCode ?? 0),
710
+ notification_seq: Number(n.seq),
711
+ initiator_id: uinOf(n.user1?.uid),
712
+ target_user_id: uinOf(n.user2?.uid),
713
+ state: "pending",
714
+ operator_id: null
715
+ }),
716
+ 8: (n, uinOf) => adminChangeEntity(n, uinOf, true),
717
+ 12: (n, uinOf) => adminChangeEntity(n, uinOf, false),
718
+ 13: (n, uinOf) => adminChangeEntity(n, uinOf, false),
719
+ 9: (n, uinOf) => kickEntity(n, uinOf),
720
+ 10: (n, uinOf) => kickEntity(n, uinOf),
721
+ 11: (n, uinOf) => ({
722
+ type: "quit",
723
+ group_id: Number(n.group?.groupCode ?? 0),
724
+ notification_seq: Number(n.seq),
725
+ target_user_id: uinOf(n.user1?.uid)
726
+ })
727
+ };
728
+ /** admin_change 实体(8/12/13 共用)。 */
729
+ function adminChangeEntity(n, uinOf, isSet) {
730
+ return {
731
+ type: "admin_change",
732
+ group_id: Number(n.group?.groupCode ?? 0),
733
+ notification_seq: Number(n.seq),
734
+ target_user_id: uinOf(n.user1?.uid),
735
+ is_set: isSet,
736
+ operator_id: uinOf(n.user2?.uid)
737
+ };
738
+ }
739
+ /** kick 实体(9/10 共用)。 */
740
+ function kickEntity(n, uinOf) {
741
+ return {
742
+ type: "kick",
743
+ group_id: Number(n.group?.groupCode ?? 0),
744
+ notification_seq: Number(n.seq),
745
+ target_user_id: uinOf(n.user1?.uid),
746
+ operator_id: uinOf(n.user2?.uid)
747
+ };
748
+ }
749
+ /** GroupNotify → 实体(isFiltered 透传给 join_request;未识别类型 null)。 */
750
+ function notifyToEntity(notify, isFiltered, uidToUin) {
751
+ const uinOf = (uid) => uid === void 0 || uid === "" ? 0 : Number(uidToUin.get(uid) ?? 0) || 0;
752
+ const builder = NOTIFY_ENTITY_BUILDERS[notify.type];
753
+ if (builder === void 0) return null;
754
+ const entity = builder(notify, uinOf);
755
+ if (entity.type === "join_request") entity.is_filtered = isFiltered;
756
+ return entity;
757
+ }
758
+ //#endregion
759
+ //#region src/milky/action/group-file.ts
760
+ const groupIdField = { group_id: z.number().int().min(10001) };
761
+ const fileIdField = { file_id: z.string().min(1) };
762
+ const optStr = z.string().nullable().optional();
763
+ /** get_group_files:获取群文件列表(根目录或指定文件夹;过滤 parent 匹配项)。 */
764
+ var GetGroupFilesAction = class extends MilkyApiAction {
765
+ name = "get_group_files";
766
+ schema = z.object({
767
+ ...groupIdField,
768
+ parent_folder_id: optStr
769
+ });
770
+ async _handle(payload) {
771
+ const groupCode = String(payload.group_id);
772
+ const parent = payload.parent_folder_id ?? "";
773
+ const items = await this.api.richMediaApi.getGroupFileList(groupCode, parent);
774
+ const files = [];
775
+ const folders = [];
776
+ for (const item of items) if (item.fileInfo !== void 0) files.push(fileToEntity(item.fileInfo, payload.group_id));
777
+ else if (item.folderInfo !== void 0) folders.push(folderToEntity(item.folderInfo, payload.group_id));
778
+ return {
779
+ files,
780
+ folders
781
+ };
782
+ }
783
+ };
784
+ /** move_group_file:移动文件(target_folder_id 空 = 移到根目录;parent 入参仅 schema 兼容)。 */
785
+ var MoveGroupFileAction = class extends MilkyApiAction {
786
+ name = "move_group_file";
787
+ schema = z.object({
788
+ ...groupIdField,
789
+ ...fileIdField,
790
+ parent_folder_id: optStr,
791
+ target_folder_id: optStr
792
+ });
793
+ async _handle(payload) {
794
+ await this.api.richMediaApi.moveGroupFile(String(payload.group_id), [payload.file_id], payload.target_folder_id ?? "");
795
+ return {};
796
+ }
797
+ };
798
+ /** rename_group_file:重命名文件。 */
799
+ var RenameGroupFileAction = class extends MilkyApiAction {
800
+ name = "rename_group_file";
801
+ schema = z.object({
802
+ ...groupIdField,
803
+ ...fileIdField,
804
+ parent_folder_id: optStr,
805
+ new_file_name: z.string().min(1)
806
+ });
807
+ async _handle(payload) {
808
+ await this.api.richMediaApi.renameGroupFile(String(payload.group_id), payload.file_id, payload.new_file_name);
809
+ return {};
810
+ }
811
+ };
812
+ /** delete_group_file:删除文件。 */
813
+ var DeleteGroupFileAction = class extends MilkyApiAction {
814
+ name = "delete_group_file";
815
+ schema = z.object({
816
+ ...groupIdField,
817
+ ...fileIdField
818
+ });
819
+ async _handle(payload) {
820
+ await this.api.richMediaApi.deleteGroupFile(String(payload.group_id), [payload.file_id]);
821
+ return {};
822
+ }
823
+ };
824
+ /**
825
+ * persist_group_file:转存文件(RichMediaApi.transGroupFile,转存语义待对齐)。
826
+ */
827
+ var PersistGroupFileAction = class extends MilkyApiAction {
828
+ name = "persist_group_file";
829
+ schema = z.object({
830
+ ...groupIdField,
831
+ ...fileIdField
832
+ });
833
+ async _handle(payload) {
834
+ await this.api.richMediaApi.transGroupFile(String(payload.group_id), payload.file_id);
835
+ return {};
836
+ }
837
+ };
838
+ /** create_group_folder:创建文件夹(返回 folder_id)。 */
839
+ var CreateGroupFolderAction = class extends MilkyApiAction {
840
+ name = "create_group_folder";
841
+ schema = z.object({
842
+ ...groupIdField,
843
+ folder_name: z.string().min(1)
844
+ });
845
+ async _handle(payload) {
846
+ return { folder_id: await this.api.richMediaApi.createGroupFolder(String(payload.group_id), payload.folder_name) };
847
+ }
848
+ };
849
+ /** delete_group_folder:删除文件夹。 */
850
+ var DeleteGroupFolderAction = class extends MilkyApiAction {
851
+ name = "delete_group_folder";
852
+ schema = z.object({
853
+ ...groupIdField,
854
+ folder_id: z.string().min(1)
855
+ });
856
+ async _handle(payload) {
857
+ await this.api.richMediaApi.deleteGroupFolder(String(payload.group_id), payload.folder_id);
858
+ return {};
859
+ }
860
+ };
861
+ /** GroupFileItemInfo → GroupFileEntity(fileSize 原生为字符串;无源字段兜底)。 */
862
+ function fileToEntity(info, groupId) {
863
+ return {
864
+ group_id: groupId,
865
+ file_id: info.fileId,
866
+ file_name: info.fileName,
867
+ parent_folder_id: info.parent ?? "",
868
+ file_size: Number(info.fileSize) || 0,
869
+ uploaded_time: Number(info.uploadTime) || 0,
870
+ expire_time: null,
871
+ uploader_id: Number(info.uploaderUin) || 0,
872
+ downloaded_times: 0
873
+ };
874
+ }
875
+ /** GroupFolderInfo → GroupFolderEntity(元数据无类型字段防御读取,缺省 0)。 */
876
+ function folderToEntity(info, groupId) {
877
+ const num = (key) => {
878
+ const v = info[key];
879
+ return typeof v === "number" ? v : Number(v) || 0;
880
+ };
881
+ return {
882
+ group_id: groupId,
883
+ folder_id: info.folderId,
884
+ parent_folder_id: info.folderUid ?? "",
885
+ folder_name: info.folderName,
886
+ created_time: num("createTime"),
887
+ last_modified_time: num("modifyTime"),
888
+ creator_id: 0,
889
+ file_count: num("totalFileCount")
890
+ };
891
+ }
892
+ //#endregion
73
893
  //#region src/milky/helper/media-uri.ts
74
894
  /**
75
895
  * 发送媒体 URI 解析(Milky OutgoingSegment uri → 本地路径)
@@ -114,11 +934,13 @@ async function writeBase64(payload, mediaDir) {
114
934
  /**
115
935
  * Milky 消息段 ↔ canonical 元素转换(纯函数,ADR-008)
116
936
  *
117
- * 接收方向:canonical → IncomingSegment(规范 14 种中 P0 映射 12 种;
118
- * market_face / markdown 无 canonical 对应,unknown 一律跳过——规范兼容性
119
- * 要求「数组中未知项跳过」)。
120
- * 发送方向:OutgoingSegment → canonical(10 种中 P0 映射 9 种;forward 发送
121
- * 走专用内核链路,P0 显式报错不静默跳过)。
937
+ * 接收方向:canonical → IncomingSegment(规范 14 种中映射 12 种;market_face /
938
+ * markdown 无 canonical 对应(MFACE/MARKDOWN 原始元素形状未探测),unknown 一律
939
+ * 跳过——规范兼容性要求「数组中未知项跳过」)。forward 接收段不经 canonical:
940
+ * kernel 无 MULTI_FORWARD 分支,adapter 从 RawMessage 提取 resId 后经
941
+ * incomingForwardSegment + ctx 注入。
942
+ * 发送方向:OutgoingSegment → canonical(10 种中映射 9 种;forward 发送需要
943
+ * kernel 组装任意内容的新转发(未探测),显式报错不静默跳过)。
122
944
  */
123
945
  /** uin 字符串 → Milky user_id(非有限正数 = 转换失败)。 */
124
946
  function toUserId(uin) {
@@ -191,7 +1013,6 @@ const INCOMING_MAPPERS = {
191
1013
  voice: voiceToIncoming,
192
1014
  video: videoToIncoming,
193
1015
  file: fileToIncoming,
194
- forward: forwardToIncoming,
195
1016
  json: (el) => ({
196
1017
  type: "light_app",
197
1018
  data: {
@@ -252,13 +1073,18 @@ function fileToIncoming(el) {
252
1073
  }
253
1074
  };
254
1075
  }
255
- /** canonical forward → forward 段(title/preview/summary 无 canonical 来源,空兜底)。 */
256
- function forwardToIncoming(el) {
1076
+ /**
1077
+ * 合并转发 → forward 段(P1)。
1078
+ * 接收方向 canonical 不承载 MULTI_FORWARD(kernel toCanonicalElement 无此分支),
1079
+ * 由 adapter 从 RawMessage 提取 resId 后调用本函数经 ctx 注入(翻译保持纯函数)。
1080
+ * title 取 NT fileName(转发卡片标题语义)。
1081
+ */
1082
+ function incomingForwardSegment(resId, title) {
257
1083
  return {
258
1084
  type: "forward",
259
1085
  data: {
260
- forward_id: el.messageIds[0] ?? "",
261
- title: "",
1086
+ forward_id: resId,
1087
+ title,
262
1088
  preview: [],
263
1089
  summary: ""
264
1090
  }
@@ -353,7 +1179,7 @@ function outgoingSegmentsToCanonical(segments) {
353
1179
  raw: seg.data.json_payload
354
1180
  });
355
1181
  break;
356
- case "forward": throw kernelError("forward 段发送暂未支持(P1 接内核转发链路)", "INVALID_PARAM");
1182
+ case "forward": throw kernelError("forward 段发送暂未支持(需 kernel 组装任意内容的新转发,P2 探测)", "INVALID_PARAM");
357
1183
  }
358
1184
  return {
359
1185
  elements,
@@ -392,7 +1218,7 @@ function fileUriToPath(uri) {
392
1218
  * 即被撤回原消息的发送者,口径待真实事件校准)。
393
1219
  */
394
1220
  /** 毫秒 → 秒(Unix 时间戳)。 */
395
- const MS_TO_SEC = 1e3;
1221
+ const MS_TO_SEC$1 = 1e3;
396
1222
  /** chatType → message_scene。 */
397
1223
  function chatTypeToScene(chatType) {
398
1224
  if (chatType === ChatType.GROUP) return "group";
@@ -410,8 +1236,9 @@ function toMessageReceiveEvent(msg, ctx) {
410
1236
  ...ctx.uidToUin !== void 0 ? { uidToUin: ctx.uidToUin } : {},
411
1237
  ...ctx.replyMeta !== void 0 ? { replyMeta: ctx.replyMeta } : {}
412
1238
  });
1239
+ if (ctx.forwardSegments !== void 0) segments.push(...ctx.forwardSegments);
413
1240
  const scene = chatTypeToScene(msg.chatType);
414
- const time = Math.floor(Number(msg.msgTime) / MS_TO_SEC);
1241
+ const time = Math.floor(Number(msg.msgTime) / MS_TO_SEC$1);
415
1242
  let data;
416
1243
  if (scene === "group") data = {
417
1244
  message_scene: "group",
@@ -455,7 +1282,7 @@ function toMessageRecallEvent(msg, ctx) {
455
1282
  const scene = chatTypeToScene(msg.chatType);
456
1283
  const operatorUin = ctx.uidToUin?.get(revoke.operatorUid) ?? revoke.operatorUid;
457
1284
  return {
458
- time: Math.floor(Number(msg.msgTime) / MS_TO_SEC),
1285
+ time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$1),
459
1286
  self_id: Number(ctx.selfUin),
460
1287
  event_type: "message_recall",
461
1288
  data: {
@@ -468,78 +1295,24 @@ function toMessageRecallEvent(msg, ctx) {
468
1295
  }
469
1296
  };
470
1297
  }
471
- //#endregion
472
- //#region src/milky/action/error-map.ts
473
- const milkyErrorCodeMap = {
474
- SEND_FAILED: -500,
475
- PERMISSION_DENIED: -405,
476
- NOT_FOUND: -404,
477
- TIMEOUT: -504,
478
- NOT_LOGIN: -403,
479
- INVALID_PARAM: -400,
480
- INVALID_STATE: -409,
481
- UNKNOWN: -500
482
- };
483
- //#endregion
484
- //#region src/milky/action/base-action.ts
485
- /**
486
- * MilkyAction:Milky 动作基类(zod 校验 + KernelError → 负数 retcode)
487
- *
488
- * 与 OB11 BaseAction 分离的原因:Milky 响应信封无 echo、参数解析失败恒
489
- * retcode=-400(规范),不复用 OB11 的 400/1400 语义。
490
- * 响应信封由 transport 组装(status/retcode/data/message)。
491
- */
492
- /** zod 错误摘要(单行,issues 取前 3 条)。 */
493
- function zodSummary(error) {
494
- const parts = [];
495
- for (const issue of error.issues.slice(0, 3)) parts.push(`${issue.path.map(String).join(".")}: ${issue.message}`);
496
- return parts.join("; ");
497
- }
498
- var MilkyAction = class {
499
- /** 校验并执行(HTTP /api 入口;WS /event 不承载动作)。 */
500
- async handle(payload) {
501
- const parsed = this.schema.safeParse(payload);
502
- if (!parsed.success) return {
503
- status: "failed",
504
- retcode: -400,
505
- data: null,
506
- message: `参数解析失败: ${zodSummary(parsed.error)}`
507
- };
508
- try {
509
- return {
510
- status: "ok",
511
- retcode: 0,
512
- data: await this._handle(parsed.data),
513
- message: ""
514
- };
515
- } catch (err) {
516
- if (isKernelError(err)) return {
517
- status: "failed",
518
- retcode: this.errorCodeMap[err.code] ?? this.errorCodeMap.UNKNOWN,
519
- data: null,
520
- message: err.message
521
- };
522
- return {
523
- status: "failed",
524
- retcode: this.errorCodeMap.UNKNOWN,
525
- data: null,
526
- message: err instanceof Error ? err.message : "未知错误"
527
- };
528
- }
529
- }
530
- };
531
- /**
532
- * 持 MilkyApi 的动作基类:api 注入 + 统一错误映射表(milkyErrorCodeMap)收拢,
533
- * 动作子类只写 name / schema / _handle。
534
- */
535
- var MilkyApiAction = class extends MilkyAction {
536
- errorCodeMap = milkyErrorCodeMap;
537
- api;
538
- constructor(api) {
539
- super();
540
- this.api = api;
541
- }
542
- };
1298
+ /** 群文件上传事件(群场景文件消息 → group_file_upload,替代 message_receive)。 */
1299
+ function toGroupFileUploadEvent(msg, selfUin) {
1300
+ if (msg.chatType !== ChatType.GROUP) return null;
1301
+ const file = msg.elements.find((el) => el.fileElement !== void 0)?.fileElement;
1302
+ if (file === void 0) return null;
1303
+ return {
1304
+ time: Math.floor(Number(msg.msgTime) / MS_TO_SEC$1),
1305
+ self_id: Number(selfUin),
1306
+ event_type: "group_file_upload",
1307
+ data: {
1308
+ group_id: Number(msg.peerUid),
1309
+ user_id: Number(msg.senderUin) || 0,
1310
+ file_id: file.fileUuid ?? file.fileName ?? "",
1311
+ file_name: file.fileName ?? "",
1312
+ file_size: Number(file.fileSize ?? 0)
1313
+ }
1314
+ };
1315
+ }
543
1316
  //#endregion
544
1317
  //#region src/milky/action/message.ts
545
1318
  /**
@@ -577,15 +1350,7 @@ var SendGroupMessageAction = class extends MilkyApiAction {
577
1350
  /** get_message:获取单条消息(seq → msgId 反查 + 实体回填)。 */
578
1351
  var GetMessageAction = class extends MilkyApiAction {
579
1352
  name = "get_message";
580
- schema = z.object({
581
- message_scene: z.enum([
582
- "friend",
583
- "group",
584
- "temp"
585
- ]),
586
- peer_id: z.number().int().min(10001),
587
- message_seq: z.number().int().min(0)
588
- });
1353
+ schema = z.object({ ...peerMessageFields });
589
1354
  async _handle(payload) {
590
1355
  const peer = await this.api.resolvePeer(payload.message_scene, payload.peer_id);
591
1356
  const msgId = await this.api.resolveMsgIdBySeq(peer, payload.message_seq, peer.chatType);
@@ -605,8 +1370,7 @@ var GetMessageAction = class extends MilkyApiAction {
605
1370
  return toMessageReceiveEvent(msg, ctx).data;
606
1371
  }
607
1372
  };
608
- /** recall_private_message:撤回私聊消息。 */
609
- var RecallPrivateMessageAction = class extends MilkyApiAction {
1373
+ /** recall_private_message:撤回私聊消息。 */ var RecallPrivateMessageAction = class extends MilkyApiAction {
610
1374
  name = "recall_private_message";
611
1375
  schema = z.object({
612
1376
  user_id: z.number().int().min(10001),
@@ -678,10 +1442,242 @@ async function resolveElement(el, uinToUid, api) {
678
1442
  function isRemoteMediaUri(el) {
679
1443
  return (el.type === "image" || el.type === "voice" || el.type === "video") && (el.path.startsWith("http://") || el.path.startsWith("https://") || el.path.startsWith("base64://"));
680
1444
  }
1445
+ /** get_history_messages:获取历史消息(start seq 锚点 → 原生翻页 → 升序输出)。 */
1446
+ var GetHistoryMessagesAction = class extends MilkyApiAction {
1447
+ name = "get_history_messages";
1448
+ schema = z.object({
1449
+ message_scene: z.enum([
1450
+ "friend",
1451
+ "group",
1452
+ "temp"
1453
+ ]),
1454
+ peer_id: z.number().int().min(10001),
1455
+ start_message_seq: z.number().int().min(0).nullable().optional(),
1456
+ limit: z.number().int().min(0).nullable().optional()
1457
+ });
1458
+ async _handle(payload) {
1459
+ const limit = payload.limit ?? HISTORY_DEFAULT_LIMIT;
1460
+ const peer = await this.api.resolvePeer(payload.message_scene, payload.peer_id);
1461
+ const anchor = await this.resolveAnchor(peer, payload.start_message_seq);
1462
+ const msgs = await this.api.msgApi.fetchMessages(peer, {
1463
+ count: limit,
1464
+ ...anchor !== void 0 ? { msgId: anchor } : {}
1465
+ });
1466
+ for (const m of msgs) this.api.seqIndex.record(m);
1467
+ const groupData = payload.message_scene === "group" ? await this.loadGroupData(peer.peerUid) : null;
1468
+ const ordered = [...msgs].reverse();
1469
+ const messages = ordered.map((m) => this.translateHistoryMessage(m, peer.peerUid, groupData));
1470
+ const hasMore = msgs.length >= limit;
1471
+ const oldest = ordered[0];
1472
+ return {
1473
+ messages,
1474
+ next_message_seq: hasMore && oldest !== void 0 ? Number(oldest.msgSeq) : null
1475
+ };
1476
+ }
1477
+ /** start_message_seq → 锚点 msgId(给了 seq 却反查不到 = NOT_FOUND)。 */
1478
+ async resolveAnchor(peer, startSeq) {
1479
+ if (startSeq === void 0 || startSeq === null) return;
1480
+ const msgId = await this.api.resolveMsgIdBySeq(peer, startSeq, peer.chatType);
1481
+ if (msgId === void 0) throw kernelError(`起始消息 ${startSeq} 不存在`, "NOT_FOUND");
1482
+ return msgId;
1483
+ }
1484
+ /** 群场景回填数据(详情 + 成员表;缓存未装配返回空数据)。 */
1485
+ async loadGroupData(groupCode) {
1486
+ if (this.api.groupCache === void 0) return {
1487
+ detail: void 0,
1488
+ members: /* @__PURE__ */ new Map()
1489
+ };
1490
+ const [detail, members] = await Promise.all([this.api.groupCache.getGroupDetail(groupCode).catch(() => void 0), this.api.groupCache.getMembers(groupCode).catch(() => [])]);
1491
+ const memberByUid = /* @__PURE__ */ new Map();
1492
+ for (const m of members) memberByUid.set(m.uid, m);
1493
+ return {
1494
+ detail,
1495
+ members: memberByUid
1496
+ };
1497
+ }
1498
+ /** 单条历史消息 → IncomingMessage(群场景注入实体,fail-soft)。 */
1499
+ translateHistoryMessage(msg, peerUid, groupData) {
1500
+ const ctx = { selfUin: this.api.self.uin };
1501
+ if (groupData !== null) {
1502
+ if (groupData.detail !== void 0) ctx.groupEntity = toGroupEntity(groupData.detail, peerUid);
1503
+ const member = groupData.members.get(msg.senderUid);
1504
+ if (member !== void 0) ctx.groupMemberEntity = toGroupMemberEntity(member, peerUid, msg.senderUin);
1505
+ }
1506
+ return toMessageReceiveEvent(msg, ctx).data;
1507
+ }
1508
+ };
1509
+ /** get_forwarded_messages:获取合并转发内容(resId → 转发索引 → fetchForwardMessage)。 */
1510
+ var GetForwardedMessagesAction = class extends MilkyApiAction {
1511
+ name = "get_forwarded_messages";
1512
+ schema = z.object({ forward_id: z.string().min(1) });
1513
+ async _handle(payload) {
1514
+ const entry = this.api.seqIndex.resolveForward(payload.forward_id);
1515
+ if (entry === void 0) throw kernelError(`转发消息 ${payload.forward_id} 不存在(未登记)`, "NOT_FOUND");
1516
+ const peer = {
1517
+ chatType: entry.chatType,
1518
+ peerUid: entry.peerUid
1519
+ };
1520
+ const msgs = await this.api.msgApi.fetchForwardMessage(peer, entry.msgId);
1521
+ for (const m of msgs) this.api.seqIndex.record(m);
1522
+ const uidToUin = await this.collectUidToUin(msgs);
1523
+ const messages = [];
1524
+ for (const m of msgs) messages.push({
1525
+ message_seq: Number(m.msgSeq),
1526
+ sender_name: await this.senderNameOf(m),
1527
+ avatar_url: avatarUrlOf(m.senderUin),
1528
+ time: Math.floor(Number(m.msgTime) / 1e3),
1529
+ segments: receiveSegmentsOf(m, uidToUin)
1530
+ });
1531
+ return { messages };
1532
+ }
1533
+ /** at 段 uid → uin 批量转换(mention 翻译;失败退空 Map)。 */
1534
+ async collectUidToUin(msgs) {
1535
+ const uids = /* @__PURE__ */ new Set();
1536
+ for (const m of msgs) for (const el of toCanonicalElements(m)) if (el.type === "at" && el.target !== "all") uids.add(el.target);
1537
+ if (uids.size === 0) return /* @__PURE__ */ new Map();
1538
+ return await this.api.uidToUin([...uids]).catch(() => /* @__PURE__ */ new Map());
1539
+ }
1540
+ /** 发送者名(群场景成员缓存昵称/名片,失败退 uin 字符串)。 */
1541
+ async senderNameOf(msg) {
1542
+ if (this.api.groupCache !== void 0 && msg.chatType === ChatType.GROUP) {
1543
+ const member = await this.api.groupCache.getMember(msg.peerUid, msg.senderUid).catch(() => void 0);
1544
+ const name = member?.cardName !== void 0 && member.cardName !== "" ? member.cardName : member?.nick;
1545
+ if (name !== void 0 && name !== "") return name;
1546
+ }
1547
+ return msg.senderUin;
1548
+ }
1549
+ };
1550
+ /** mark_message_as_read:标记会话已读(NT setMsgRead 以会话为粒度,seq 入参仅兼容 schema)。 */
1551
+ var MarkMessageAsReadAction = class extends MilkyApiAction {
1552
+ name = "mark_message_as_read";
1553
+ schema = z.object({ ...peerMessageFields });
1554
+ async _handle(payload) {
1555
+ const peer = await this.api.resolvePeer(payload.message_scene, payload.peer_id);
1556
+ await this.api.msgApi.markRead(peer);
1557
+ return {};
1558
+ }
1559
+ };
1560
+ /** 历史消息默认条数(规范 default 20)。 */
1561
+ const HISTORY_DEFAULT_LIMIT = 20;
1562
+ /** get_message / mark_message_as_read 共入参(场景 + 对端 + seq)。 */
1563
+ const peerMessageFields = {
1564
+ message_scene: z.enum([
1565
+ "friend",
1566
+ "group",
1567
+ "temp"
1568
+ ]),
1569
+ peer_id: z.number().int().min(10001),
1570
+ message_seq: z.number().int().min(0)
1571
+ };
1572
+ /** 单条消息 → 接收段(canonical 映射 + forward 段注入,forward 不经 canonical)。 */
1573
+ function receiveSegmentsOf(msg, uidToUin) {
1574
+ const segments = canonicalToIncomingSegments(toCanonicalElements(msg), { ...uidToUin.size > 0 ? { uidToUin } : {} });
1575
+ const fwd = msg.elements.find((el) => el.multiForwardMsgElement !== void 0)?.multiForwardMsgElement;
1576
+ if (fwd !== void 0) segments.push(incomingForwardSegment(fwd.resId, fwd.fileName));
1577
+ return segments;
1578
+ }
1579
+ /** QQ 头像 URL(公开头像服务,640px)。 */
1580
+ function avatarUrlOf(uin) {
1581
+ return uin === "" ? "" : `https://q1.qlogo.cn/g?b=qq&nk=${uin}&s=640`;
1582
+ }
1583
+ //#endregion
1584
+ //#region src/milky/action/profile.ts
1585
+ /**
1586
+ * Milky 资料类动作(P1):get_user_profile / get_friend_info / set_avatar /
1587
+ * set_nickname / set_bio / get_cookies / get_csrf_token
1588
+ *
1589
+ * set_avatar 的 uri 经 media-uri 解析(本地路径 / file:// 直用,http(s)/base64
1590
+ * 落盘 mediaDir);get_cookies 输出规范为单字符串(k=v 分号拼接);
1591
+ * get_csrf_token = qun.qq.com cookies 的 skey → bkn(OB11 get_csrf_token 同口径)。
1592
+ */
1593
+ /** CSRF 计算域名(bkn 口径,OB11 get_csrf_token 同源)。 */
1594
+ const CSRF_DOMAIN = "qun.qq.com";
1595
+ /** get_user_profile:获取用户个人信息(陌生人信息 API;country/city/school 无源空兜底)。 */
1596
+ var GetUserProfileAction = class extends MilkyApiAction {
1597
+ name = "get_user_profile";
1598
+ schema = z.object({ user_id: z.number().int().min(10001) });
1599
+ async _handle(payload) {
1600
+ const info = await this.api.profileApi.getStrangerInfo(String(payload.user_id));
1601
+ return {
1602
+ nickname: info.nickname,
1603
+ qid: info.qid,
1604
+ age: info.age,
1605
+ sex: info.sex,
1606
+ remark: info.remark,
1607
+ bio: info.long_nick,
1608
+ level: info.qq_level,
1609
+ country: "",
1610
+ city: "",
1611
+ school: ""
1612
+ };
1613
+ }
1614
+ };
1615
+ /** get_friend_info:获取好友信息(好友列表单查)。 */
1616
+ var GetFriendInfoAction = class extends MilkyApiAction {
1617
+ name = "get_friend_info";
1618
+ schema = z.object({
1619
+ user_id: z.number().int().min(10001),
1620
+ no_cache: z.boolean().nullable().optional()
1621
+ });
1622
+ async _handle(payload) {
1623
+ const uin = String(payload.user_id);
1624
+ const friend = (await this.api.friendApi.getFriendList()).find((f) => f.uin === uin);
1625
+ if (friend === void 0) throw kernelError(`好友 ${uin} 不存在`, "NOT_FOUND");
1626
+ return { friend: toFriendEntity(friend, friend.uin, friend.nickname) };
1627
+ }
1628
+ };
1629
+ /** set_avatar:设置头像(uri → 本地路径)。 */
1630
+ var SetAvatarAction = class extends MilkyApiAction {
1631
+ name = "set_avatar";
1632
+ schema = z.object({ uri: z.string().min(1) });
1633
+ async _handle(payload) {
1634
+ const path = await resolveMediaUri(payload.uri, this.api.mediaDir);
1635
+ await this.api.profileApi.setHeader(path);
1636
+ return {};
1637
+ }
1638
+ };
1639
+ /** set_nickname:设置昵称。 */
1640
+ var SetNicknameAction = class extends MilkyApiAction {
1641
+ name = "set_nickname";
1642
+ schema = z.object({ new_nickname: z.string() });
1643
+ async _handle(payload) {
1644
+ await this.api.profileApi.setNickName(payload.new_nickname);
1645
+ return {};
1646
+ }
1647
+ };
1648
+ /** set_bio:设置个性签名。 */
1649
+ var SetBioAction = class extends MilkyApiAction {
1650
+ name = "set_bio";
1651
+ schema = z.object({ new_bio: z.string() });
1652
+ async _handle(payload) {
1653
+ await this.api.profileApi.setLongNick(payload.new_bio);
1654
+ return {};
1655
+ }
1656
+ };
1657
+ /** get_cookies:获取 cookies(规范输出为 k=v 分号拼接字符串)。 */
1658
+ var GetCookiesAction = class extends MilkyApiAction {
1659
+ name = "get_cookies";
1660
+ schema = z.object({ domain: z.string().min(1) });
1661
+ async _handle(payload) {
1662
+ const cookies = await this.api.ticketApi.getCookies(payload.domain, this.api.self.uin);
1663
+ return { cookies: Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ") };
1664
+ }
1665
+ };
1666
+ /** get_csrf_token:bkn(qun.qq.com skey 派生,OB11 get_csrf_token 同口径)。 */
1667
+ var GetCSRFTokenAction = class extends MilkyApiAction {
1668
+ name = "get_csrf_token";
1669
+ schema = z.object({}).strict();
1670
+ async _handle() {
1671
+ const cookies = await this.api.ticketApi.getCookies(CSRF_DOMAIN, this.api.self.uin);
1672
+ return { csrf_token: TicketApi.getBkn(cookies["skey"] ?? "") };
1673
+ }
1674
+ };
681
1675
  //#endregion
682
1676
  //#region src/milky/action/social.ts
683
1677
  /**
684
- * Milky 社交类动作(P0):get_friend_list / get_group_list
1678
+ * Milky 社交类动作(P0:get_friend_list / get_group_list;
1679
+ * P1 +5:send_profile_like / delete_friend / get_friend_requests /
1680
+ * accept_friend_request / reject_friend_request)
685
1681
  */
686
1682
  const emptySchema$1 = z.object({}).strict();
687
1683
  /** get_friend_list:获取好友列表。 */
@@ -692,8 +1688,7 @@ var GetFriendListAction = class extends MilkyApiAction {
692
1688
  return { friends: (await this.api.friendApi.getFriendList()).map((f) => toFriendEntity(f, f.uin, f.nickname)) };
693
1689
  }
694
1690
  };
695
- /**
696
- * get_group_list:获取群列表。
1691
+ /** get_group_list:获取群列表。
697
1692
  * 实测校准(2026-08-08,OB11 同款):原生 getGroupList 返回值无数据,列表经
698
1693
  * onGroupListUpdate 事件进 GroupCache——缓存优先,为空触发刷新回填。
699
1694
  */
@@ -704,6 +1699,85 @@ var GetGroupListAction = class extends MilkyApiAction {
704
1699
  return { groups: (this.api.groupCache !== void 0 ? await this.api.groupCache.listGroupsRefreshed() : await this.api.groupApi.getGroupList(true)).map(groupListToEntity) };
705
1700
  }
706
1701
  };
1702
+ /** send_profile_like:发送好友赞(count 缺省 1,规范 default 同值)。 */
1703
+ var SendProfileLikeAction = class extends MilkyApiAction {
1704
+ name = "send_profile_like";
1705
+ schema = z.object({
1706
+ user_id: z.number().int().min(10001),
1707
+ count: z.number().int().min(0).nullable().optional()
1708
+ });
1709
+ async _handle(payload) {
1710
+ const uid = await this.api.resolveUidFromUin(payload.user_id);
1711
+ await this.api.profileLikeApi.sendLike(uid, payload.count ?? 1);
1712
+ return {};
1713
+ }
1714
+ };
1715
+ /** delete_friend:删除好友。 */
1716
+ var DeleteFriendAction = class extends MilkyApiAction {
1717
+ name = "delete_friend";
1718
+ schema = z.object({ user_id: z.number().int().min(10001) });
1719
+ async _handle(payload) {
1720
+ const uid = await this.api.resolveUidFromUin(payload.user_id);
1721
+ await this.api.friendApi.deleteFriend(uid);
1722
+ return {};
1723
+ }
1724
+ };
1725
+ /**
1726
+ * get_friend_requests:获取好友请求列表。
1727
+ * kernel getBuddyReqList 无过滤/数量参数——is_filtered 入参仅作 schema 兼容
1728
+ * (全量返回);state 无源字段恒 pending(真实状态待探测校准)。
1729
+ */
1730
+ var GetFriendRequestsAction = class extends MilkyApiAction {
1731
+ name = "get_friend_requests";
1732
+ schema = z.object({
1733
+ limit: z.number().int().min(0).nullable().optional(),
1734
+ is_filtered: z.boolean().nullable().optional()
1735
+ });
1736
+ async _handle(payload) {
1737
+ const reqs = await this.api.friendApi.getBuddyReqList();
1738
+ let uidToUin = /* @__PURE__ */ new Map();
1739
+ if (reqs.length > 0) uidToUin = await this.api.uidToUin(reqs.map((req) => req.friendUid)).catch(() => /* @__PURE__ */ new Map());
1740
+ const requests = reqs.map((req) => ({
1741
+ time: Number(req.reqTime) || 0,
1742
+ initiator_id: Number(uidToUin.get(req.friendUid)) || 0,
1743
+ initiator_uid: req.friendUid,
1744
+ target_user_id: Number(this.api.self.uin),
1745
+ target_user_uid: this.api.self.uid ?? "",
1746
+ state: "pending",
1747
+ comment: typeof req["words"] === "string" ? req["words"] : "",
1748
+ via: req.sourceName ?? "",
1749
+ is_filtered: false
1750
+ }));
1751
+ const limit = payload.limit ?? requests.length;
1752
+ return { requests: requests.slice(0, limit) };
1753
+ }
1754
+ };
1755
+ /**
1756
+ * accept/reject_friend_request 公共基类:initiator_uid → 查申请 → 应答。
1757
+ * kernel handleFriendRequest 无 reason 参数(拒绝理由无源,入参仅 schema 兼容)。
1758
+ */
1759
+ var HandleFriendRequestBase = class extends MilkyApiAction {
1760
+ schema = z.object({
1761
+ initiator_uid: z.string().min(1),
1762
+ is_filtered: z.boolean().nullable().optional(),
1763
+ reason: z.string().nullable().optional()
1764
+ });
1765
+ async _handle(payload) {
1766
+ const notify = await findBuddyReq(this.api, payload.initiator_uid);
1767
+ await this.api.friendApi.handleFriendRequest(notify, this.accept);
1768
+ return {};
1769
+ }
1770
+ };
1771
+ /** accept_friend_request:同意好友申请。 */
1772
+ var AcceptFriendRequestAction = class extends HandleFriendRequestBase {
1773
+ name = "accept_friend_request";
1774
+ accept = true;
1775
+ };
1776
+ /** reject_friend_request:拒绝好友申请。 */
1777
+ var RejectFriendRequestAction = class extends HandleFriendRequestBase {
1778
+ name = "reject_friend_request";
1779
+ accept = false;
1780
+ };
707
1781
  //#endregion
708
1782
  //#region src/milky/action/system.ts
709
1783
  /**
@@ -743,14 +1817,13 @@ var GetImplInfoAction = class extends MilkyApiAction {
743
1817
  //#endregion
744
1818
  //#region src/milky/action/index.ts
745
1819
  /**
746
- * Milky 动作注册表(P0:9 个动作)
1820
+ * Milky 动作注册表(P0 9 个 + P1 40 个 = 49 个)
747
1821
  *
748
- * P0 面 = docs/protocols/milky.md §9 P0 清单:get_login_info / get_impl_info /
749
- * send_private_message / send_group_message / get_message / recall_*_message /
750
- * get_friend_list / get_group_list。P1 扩面(群管/精华/群文件/好友请求/票据)
751
- * 往本注册表追加。
1822
+ * P0 面 = docs/protocols/milky.md §9 P0 清单;P1 面 = §9 P1 清单(群管/精华/
1823
+ * 群文件/好友操作/票据/历史与转发)。延后项(nudge 发送 / rename_folder /
1824
+ * download_url / resource 反查 / 置顶 / 公告 / 上传)见 milky.md §6。
752
1825
  */
753
- /** Milky 动作注册表(P0 9 动作)。 */
1826
+ /** Milky 动作注册表(P0+P1 49 动作)。 */
754
1827
  function createMilkyActionRegistry(opts) {
755
1828
  const registry = new ActionRegistry();
756
1829
  const { api } = opts;
@@ -763,84 +1836,194 @@ function createMilkyActionRegistry(opts) {
763
1836
  registry.register(new RecallGroupMessageAction(api));
764
1837
  registry.register(new GetFriendListAction(api));
765
1838
  registry.register(new GetGroupListAction(api));
1839
+ registry.register(new GetUserProfileAction(api));
1840
+ registry.register(new GetFriendInfoAction(api));
1841
+ registry.register(new SetAvatarAction(api));
1842
+ registry.register(new SetNicknameAction(api));
1843
+ registry.register(new SetBioAction(api));
1844
+ registry.register(new GetCookiesAction(api));
1845
+ registry.register(new GetCSRFTokenAction(api));
1846
+ registry.register(new SendProfileLikeAction(api));
1847
+ registry.register(new DeleteFriendAction(api));
1848
+ registry.register(new GetFriendRequestsAction(api));
1849
+ registry.register(new AcceptFriendRequestAction(api));
1850
+ registry.register(new RejectFriendRequestAction(api));
1851
+ registry.register(new GetHistoryMessagesAction(api));
1852
+ registry.register(new GetForwardedMessagesAction(api));
1853
+ registry.register(new MarkMessageAsReadAction(api));
1854
+ registry.register(new GetGroupInfoAction(api));
1855
+ registry.register(new GetGroupMemberListAction(api));
1856
+ registry.register(new GetGroupMemberInfoAction(api));
1857
+ registry.register(new SetGroupNameAction(api));
1858
+ registry.register(new SetGroupMemberCardAction(api));
1859
+ registry.register(new SetGroupMemberAdminAction(api));
1860
+ registry.register(new SetGroupMemberMuteAction(api));
1861
+ registry.register(new SetGroupWholeMuteAction(api));
1862
+ registry.register(new KickGroupMemberAction(api));
1863
+ registry.register(new QuitGroupAction(api));
1864
+ registry.register(new GetGroupEssenceMessagesAction(api));
1865
+ registry.register(new SetGroupEssenceMessageAction(api));
1866
+ registry.register(new SendGroupMessageReactionAction(api));
1867
+ registry.register(new GetGroupNotificationsAction(api));
1868
+ registry.register(new AcceptGroupRequestAction(api));
1869
+ registry.register(new RejectGroupRequestAction(api));
1870
+ registry.register(new AcceptGroupInvitationAction(api));
1871
+ registry.register(new RejectGroupInvitationAction(api));
1872
+ registry.register(new GetGroupFilesAction(api));
1873
+ registry.register(new MoveGroupFileAction(api));
1874
+ registry.register(new RenameGroupFileAction(api));
1875
+ registry.register(new DeleteGroupFileAction(api));
1876
+ registry.register(new PersistGroupFileAction(api));
1877
+ registry.register(new CreateGroupFolderAction(api));
1878
+ registry.register(new DeleteGroupFolderAction(api));
766
1879
  return registry;
767
1880
  }
768
1881
  //#endregion
769
- //#region src/milky/api.ts
770
- var MilkyApi = class {
771
- msgApi;
772
- groupApi;
773
- friendApi;
774
- profileApi;
775
- self;
776
- groupCache;
777
- qqVersion;
778
- implVersion;
779
- mediaDir;
780
- /** message_seq ↔ msgId 双向映射(接收链路 record,动作侧反查)。 */
781
- seqIndex;
782
- constructor(opts, seqIndex) {
783
- this.msgApi = opts.msgApi;
784
- this.groupApi = opts.groupApi;
785
- this.friendApi = opts.friendApi;
786
- this.profileApi = opts.profileApi;
787
- this.self = opts.self;
788
- this.groupCache = opts.groupCache;
789
- this.qqVersion = opts.qqVersion;
790
- this.implVersion = opts.implVersion;
791
- this.mediaDir = opts.mediaDir;
792
- this.seqIndex = seqIndex;
793
- }
794
- /** uin → uid(私聊目标 / at 段发送转换)。 */
795
- uinToUid(uins) {
796
- return this.groupApi.uinToUid(uins);
797
- }
798
- /** uid → uin(事件路径 mention 转换)。 */
799
- uidToUin(uids) {
800
- return this.groupApi.uidToUin(uids);
801
- }
802
- /**
803
- * message_scene + 数字 ID → NT Peer。
804
- * group = 群号直作 peerUid;friend/temp = uin → uid(失败退 uin 原样,交 QQ 端判断)。
805
- */
806
- async resolvePeer(scene, id) {
807
- if (scene === "group") return {
808
- chatType: ChatType.GROUP,
809
- peerUid: String(id)
810
- };
811
- const uin = String(id);
812
- let uid;
813
- try {
814
- uid = (await this.uinToUid([uin])).get(uin);
815
- } catch {}
816
- return {
817
- chatType: scene === "friend" ? ChatType.C2C : ChatType.C2C_TEMP,
818
- peerUid: uid ?? uin
819
- };
820
- }
821
- /**
822
- * seq → msgId(索引优先,未登记扫描近段历史兜底)。
823
- * 两层都未命中返回 undefined(调用方抛 NOT_FOUND)。
824
- */
825
- async resolveMsgIdBySeq(peer, seq, chatType) {
826
- const indexed = this.seqIndex.resolveMsgId(chatType, peer.peerUid, seq);
827
- if (indexed !== void 0) return indexed;
828
- const hit = (await this.msgApi.fetchMessages(peer, { count: 60 }).catch(() => [])).find((m) => Number(m.msgSeq) === seq);
829
- if (hit === void 0) return;
830
- this.seqIndex.record(hit);
831
- return hit.msgId;
832
- }
833
- /** 发送结果回查:msgId → {message_seq, time}(sendMessage 只回 msgId)。 */
834
- async resolveSentOutput(peer, msgId) {
835
- const [sent] = await this.msgApi.fetchMsgsByMsgId(peer, [msgId]);
836
- if (sent === void 0) throw kernelError("发送结果回查失败(msgId 未命中)", "UNKNOWN");
837
- this.seqIndex.record(sent);
838
- return {
839
- message_seq: Number(sent.msgSeq),
840
- time: Math.floor(Number(sent.msgTime) / 1e3)
841
- };
842
- }
1882
+ //#region src/milky/helper/notice.ts
1883
+ /** 毫秒 → 秒(Unix 时间戳)。 */
1884
+ const MS_TO_SEC = 1e3;
1885
+ /** uid → uin(core 共享解析;ctx 携带映射)。 */
1886
+ function toUin(uid, ctx) {
1887
+ return resolveUin(uid, ctx.uidToUin);
1888
+ }
1889
+ /** 事件信封公共字段。 */
1890
+ function envelope(msg, ctx, eventType, data) {
1891
+ return {
1892
+ time: Math.floor(Number(msg.msgTime) / MS_TO_SEC),
1893
+ self_id: Number(ctx.selfUin),
1894
+ event_type: eventType,
1895
+ data
1896
+ };
1897
+ }
1898
+ /** 单个 grayTip → Milky notice 事件(null = 不可翻译)。 */
1899
+ function translateGrayTip(msg, g, ctx) {
1900
+ if (g.aioOpGrayTipElement !== void 0) return aioOpToEvent(msg, g, ctx);
1901
+ if (g.subElementType === GrayTipSubType.GROUP) return groupElementToEvent(msg, g.groupElement, ctx);
1902
+ return null;
1903
+ }
1904
+ /** 群成员变动类映射器表(键 = TipGroupElementType;缺项 = 不可翻译返回 null)。 */
1905
+ const GROUP_CHANGE_MAPPERS = {
1906
+ [TipGroupElementType.MEMBER_ADD]: toMemberIncrease,
1907
+ [TipGroupElementType.QUIT]: toMemberDecrease,
1908
+ [TipGroupElementType.SHUT_UP]: toShutUpEvent,
1909
+ [TipGroupElementType.GROUP_NAME_MODIFIED]: toNameChange,
1910
+ [TipGroupElementType.DISBANDED]: toDisband
843
1911
  };
1912
+ function groupElementToEvent(msg, grp, ctx) {
1913
+ if (grp === void 0 || grp.type === void 0) return null;
1914
+ const mapper = GROUP_CHANGE_MAPPERS[grp.type];
1915
+ if (mapper === void 0) return null;
1916
+ return mapper(msg, groupChangeCommon(grp, Number(msg.peerUid)), grp, ctx);
1917
+ }
1918
+ /** 禁言载荷优先(shutUp.member/admin 覆盖顶层 memberUid/adminUid)。 */
1919
+ function groupChangeCommon(grp, groupId) {
1920
+ return {
1921
+ groupId,
1922
+ memberUid: grp.shutUp?.member?.uid ?? grp.memberUid ?? "",
1923
+ adminUid: grp.shutUp?.admin?.uid ?? grp.adminUid ?? ""
1924
+ };
1925
+ }
1926
+ /** 群成员增加(管理员同意 → operator_id;邀请入群 → invitor_id)。 */
1927
+ function toMemberIncrease(msg, common, _grp, ctx) {
1928
+ const { groupId, memberUid, adminUid } = common;
1929
+ return envelope(msg, ctx, "group_member_increase", {
1930
+ group_id: groupId,
1931
+ user_id: toUin(memberUid, ctx),
1932
+ operator_id: adminUid !== "" && adminUid === memberUid ? toUin(adminUid, ctx) : null,
1933
+ invitor_id: adminUid !== "" && adminUid !== memberUid ? toUin(adminUid, ctx) : null
1934
+ });
1935
+ }
1936
+ /** 群成员减少(操作者与本人不同 = 被踢;主动退群无操作者)。 */
1937
+ function toMemberDecrease(msg, common, _grp, ctx) {
1938
+ const { groupId, memberUid, adminUid } = common;
1939
+ return envelope(msg, ctx, "group_member_decrease", {
1940
+ group_id: groupId,
1941
+ user_id: toUin(memberUid, ctx),
1942
+ operator_id: adminUid !== "" && adminUid !== memberUid ? toUin(adminUid, ctx) : null
1943
+ });
1944
+ }
1945
+ /** 禁言(member 空 = 全员禁言口径;is_mute = 是否处于禁言态)。 */
1946
+ function toShutUpEvent(msg, common, grp, ctx) {
1947
+ const { groupId, memberUid, adminUid } = common;
1948
+ const duration = Number(grp.shutUp?.duration ?? 0);
1949
+ if (memberUid === "") return envelope(msg, ctx, "group_whole_mute", {
1950
+ group_id: groupId,
1951
+ operator_id: toUin(adminUid, ctx),
1952
+ is_mute: duration > 0
1953
+ });
1954
+ return envelope(msg, ctx, "group_mute", {
1955
+ group_id: groupId,
1956
+ user_id: toUin(memberUid, ctx),
1957
+ operator_id: toUin(adminUid, ctx),
1958
+ duration
1959
+ });
1960
+ }
1961
+ /** 改名 / 解散公共守卫:操作者 uid 不可得时不产出(schema operator_id 下限)。 */
1962
+ function operatorOf(common) {
1963
+ return common.adminUid !== "" ? common.adminUid : common.memberUid;
1964
+ }
1965
+ function toNameChange(msg, common, grp, ctx) {
1966
+ const operatorUid = operatorOf(common);
1967
+ if (operatorUid === "") return null;
1968
+ return envelope(msg, ctx, "group_name_change", {
1969
+ group_id: common.groupId,
1970
+ new_group_name: grp.groupName ?? "",
1971
+ operator_id: toUin(operatorUid, ctx)
1972
+ });
1973
+ }
1974
+ function toDisband(msg, common, _grp, ctx) {
1975
+ const operatorUid = operatorOf(common);
1976
+ if (operatorUid === "") return null;
1977
+ return envelope(msg, ctx, "group_disband", {
1978
+ group_id: common.groupId,
1979
+ operator_id: toUin(operatorUid, ctx)
1980
+ });
1981
+ }
1982
+ /**
1983
+ * aioOp → 戳一戳事件(friend_nudge / group_nudge)。
1984
+ * aioOp 载荷完整字段形状未经实测(已证字段仅 operateType/peerUid):
1985
+ * sender 取消息发送者、receiver 取 aioOp.peerUid(口径与 OB11 poke 一致,待校准);
1986
+ * display_* 字段无来源,恒空串;始终打 raw 日志积累校准数据。
1987
+ */
1988
+ function aioOpToEvent(msg, g, ctx) {
1989
+ const aioOp = g.aioOpGrayTipElement;
1990
+ ctx.logger?.warn({ raw: JSON.stringify(g)?.slice(0, 2e3) }, "milky: aioOp grayTip raw(戳一戳待真实事件校准)");
1991
+ if (msg.chatType === ChatType.GROUP) return envelope(msg, ctx, "group_nudge", {
1992
+ group_id: Number(msg.peerUid),
1993
+ sender_id: toUin(msg.senderUid, ctx),
1994
+ receiver_id: toUin(aioOp?.peerUid ?? "", ctx),
1995
+ display_action: "",
1996
+ display_suffix: "",
1997
+ display_action_img_url: ""
1998
+ });
1999
+ const senderUin = toUin(msg.senderUid, ctx);
2000
+ return envelope(msg, ctx, "friend_nudge", {
2001
+ user_id: senderUin,
2002
+ is_self_send: senderUin === Number(ctx.selfUin),
2003
+ is_self_receive: senderUin !== Number(ctx.selfUin),
2004
+ display_action: "",
2005
+ display_suffix: "",
2006
+ display_action_img_url: ""
2007
+ });
2008
+ }
2009
+ /**
2010
+ * RawMessage → Milky notice 事件(无 grayTip / 不可翻译返回 null)。
2011
+ * 未知子类型打 raw JSON 日志(JSON/BUDDY/ESSENCE/FILE 等——校准数据积累入口)。
2012
+ */
2013
+ function toMilkyNoticeEvent(msg, ctx) {
2014
+ for (const el of msg.elements) {
2015
+ const g = el.grayTipElement;
2016
+ if (g === void 0) continue;
2017
+ const event = translateGrayTip(msg, g, ctx);
2018
+ if (event !== null) return event;
2019
+ }
2020
+ const untranslated = firstUntranslatedGrayTip(msg);
2021
+ if (untranslated !== void 0) ctx.logger?.warn({
2022
+ subType: untranslated.subElementType,
2023
+ raw: JSON.stringify(untranslated)?.slice(0, 2e3)
2024
+ }, "milky: 未翻译的 grayTip 子类型(raw 校准数据)");
2025
+ return null;
2026
+ }
844
2027
  //#endregion
845
2028
  //#region src/milky/helper/seq-index.ts
846
2029
  /** 单方向最大条目数(测试用例引用容量语义)。 */
@@ -850,6 +2033,8 @@ var MessageSeqIndex = class {
850
2033
  seqToMsg = /* @__PURE__ */ new Map();
851
2034
  /** msgId → seq 元信息。 */
852
2035
  msgToMeta = /* @__PURE__ */ new Map();
2036
+ /** 合并转发 resId → 定位信息(P1:get_forwarded_messages)。 */
2037
+ forwards = /* @__PURE__ */ new Map();
853
2038
  /** 接收链路登记(一条消息双向写入;重复 msgId 覆盖)。 */
854
2039
  record(msg) {
855
2040
  const seq = Number(msg.msgSeq);
@@ -886,6 +2071,23 @@ var MessageSeqIndex = class {
886
2071
  key(chatType, peerUid, seq) {
887
2072
  return `${chatType}:${peerUid}:${seq}`;
888
2073
  }
2074
+ /**
2075
+ * 登记合并转发(接收链路:消息含 multiForwardMsgElement 时调用)。
2076
+ * resId 是 Milky forward_id 的来源;同一 resId 重复覆盖。
2077
+ */
2078
+ recordForward(resId, entry) {
2079
+ if (resId === "") return;
2080
+ this.forwards.set(resId, entry);
2081
+ while (this.forwards.size > MAX_ENTRIES) {
2082
+ const oldest = this.forwards.keys().next().value;
2083
+ if (oldest === void 0) break;
2084
+ this.forwards.delete(oldest);
2085
+ }
2086
+ }
2087
+ /** resId → 转发定位(get_forwarded_messages;未登记返回 undefined)。 */
2088
+ resolveForward(resId) {
2089
+ return this.forwards.get(resId);
2090
+ }
889
2091
  /** FIFO 淘汰到容量上限内。 */
890
2092
  evict() {
891
2093
  while (this.seqToMsg.size > MAX_ENTRIES) {
@@ -1073,6 +2275,8 @@ function pathnameOf(url) {
1073
2275
  var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
1074
2276
  protocol = "milky";
1075
2277
  msgChannel;
2278
+ groupChannel;
2279
+ buddyChannel;
1076
2280
  calibLogger;
1077
2281
  api;
1078
2282
  seqIndex = new MessageSeqIndex();
@@ -1091,6 +2295,8 @@ var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
1091
2295
  }
1092
2296
  });
1093
2297
  this.msgChannel = opts.msgChannel;
2298
+ this.groupChannel = opts.groupChannel;
2299
+ this.buddyChannel = opts.buddyChannel;
1094
2300
  this.calibLogger = opts.logger;
1095
2301
  this.api = new MilkyApi(opts, this.seqIndex);
1096
2302
  this.registry = createMilkyActionRegistry({ api: this.api });
@@ -1131,37 +2337,100 @@ var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
1131
2337
  this.stopTransports = null;
1132
2338
  }
1133
2339
  }
1134
- /** 订阅 kernel 消息通道(幂等)。 */
2340
+ /** 订阅 kernel 事件(消息 + 群通知 + 好友请求,幂等)。 */
1135
2341
  subscribe() {
1136
2342
  if (this.unsubscribes.length > 0) return;
1137
2343
  this.unsubscribes.push(subscribeRawMessages(this.msgChannel, (msg) => {
1138
2344
  this.handleIncoming(msg);
1139
2345
  }));
2346
+ if (this.groupChannel !== void 0) {
2347
+ const groupChannel = this.groupChannel;
2348
+ this.unsubscribes.push(groupChannel.on("Group/onGroupNotifiesUpdated", (doubt, notifies) => {
2349
+ this.handleGroupNotifies(doubt, notifies);
2350
+ }));
2351
+ }
2352
+ if (this.buddyChannel !== void 0) {
2353
+ const buddyChannel = this.buddyChannel;
2354
+ this.unsubscribes.push(buddyChannel.on("Buddy/onBuddyReqChange", (arg) => {
2355
+ this.handleBuddyReqs(arg);
2356
+ }));
2357
+ }
1140
2358
  }
1141
2359
  /** 退订(幂等)。 */
1142
2360
  unsubscribeAll() {
1143
2361
  for (const off of this.unsubscribes) off();
1144
2362
  this.unsubscribes = [];
1145
2363
  }
1146
- /** 单条消息:seq 登记 → grayTip→recall / 普通→message_receive(自消息不上报)。 */
2364
+ /** 单条消息:seq 登记 → grayTip→notice / 群文件上传 / message_receive(自消息不上报)。 */
1147
2365
  async handleIncoming(msg) {
1148
2366
  this.seqIndex.record(msg);
2367
+ this.recordForward(msg);
1149
2368
  if (hasGrayTip(msg)) {
1150
- const event = await this.buildRecallEvent(msg);
1151
- if (event !== null) this.broadcastEvent(event);
2369
+ await this.broadcastNotice(msg);
2370
+ return;
2371
+ }
2372
+ const upload = toGroupFileUploadEvent(msg, this.api.self.uin);
2373
+ if (upload !== null) {
2374
+ this.broadcastEvent(upload);
1152
2375
  return;
1153
2376
  }
1154
2377
  if (String(msg.senderUin) === this.api.self.uin) return;
1155
- const event = await this.buildMessageReceiveEvent(msg);
1156
- this.broadcastEvent(event);
2378
+ this.broadcastEvent(await this.buildMessageReceiveEvent(msg));
2379
+ }
2380
+ /** grayTip → notice 事件广播(REVOKE 走 message_recall,其余走 notice 翻译)。 */
2381
+ async broadcastNotice(msg) {
2382
+ const uidToUin = await this.resolveUidToUin(collectGrayTipUids(msg));
2383
+ const event = findRevokeElement(msg) !== void 0 ? toMessageRecallEvent(msg, {
2384
+ selfUin: this.api.self.uin,
2385
+ ...uidToUin !== void 0 ? { uidToUin } : {}
2386
+ }) : toMilkyNoticeEvent(msg, this.noticeCtx(uidToUin));
2387
+ if (event !== null) this.broadcastEvent(event);
2388
+ }
2389
+ /** notice 翻译上下文(uidToUin 缺省空 Map;logger 透传校准日志)。 */
2390
+ noticeCtx(uidToUin) {
2391
+ return {
2392
+ selfUin: this.api.self.uin,
2393
+ ...uidToUin !== void 0 ? { uidToUin } : { uidToUin: /* @__PURE__ */ new Map() },
2394
+ ...this.calibLogger !== void 0 ? { logger: this.calibLogger } : {}
2395
+ };
2396
+ }
2397
+ /** 群通知通道:入群请求 / 成员邀请 / 邀请自身(uid→uin 批量后翻译)。 */
2398
+ async handleGroupNotifies(doubt, notifies) {
2399
+ const ctx = await this.requestCtx(collectNotifyUids(notifies));
2400
+ for (const notify of notifies) {
2401
+ const event = toGroupRequestEvent(notify, doubt, ctx);
2402
+ if (event !== null) this.broadcastEvent(event);
2403
+ }
2404
+ }
2405
+ /** 好友请求通道:Buddy/onBuddyReqChange(防御收窄,未知形状 raw 日志)。 */
2406
+ handleBuddyReqs(arg) {
2407
+ const reqs = narrowBuddyReqs(arg);
2408
+ if (reqs === null) {
2409
+ this.calibLogger?.warn({ raw: truncate(JSON.stringify(arg)) }, "milky: onBuddyReqChange 未知参数形状");
2410
+ return;
2411
+ }
2412
+ (async () => {
2413
+ const ctx = await this.requestCtx(reqs.map((req) => req.friendUid));
2414
+ for (const req of reqs) this.broadcastEvent(toFriendRequestEvent(req, ctx));
2415
+ })();
2416
+ }
2417
+ /** request 翻译上下文(uid→uin 批量转换)。 */
2418
+ async requestCtx(uids) {
2419
+ const uidToUin = await this.resolveUidToUin(uids);
2420
+ return {
2421
+ selfUin: this.api.self.uin,
2422
+ ...uidToUin !== void 0 ? { uidToUin } : { uidToUin: /* @__PURE__ */ new Map() }
2423
+ };
1157
2424
  }
1158
- /** message_receive 组装:at uid→uin + reply 回填 + 群实体回填(全部 fail-soft)。 */
2425
+ /** message_receive 组装:at uid→uin + reply 回填 + 群实体回填 + 转发段(全部 fail-soft)。 */
1159
2426
  async buildMessageReceiveEvent(msg) {
1160
2427
  const elements = toCanonicalElements(msg);
1161
2428
  const ctx = { selfUin: this.api.self.uin };
1162
2429
  await this.fillMentionUids(elements, ctx);
1163
2430
  this.fillReplyMeta(elements, ctx);
1164
2431
  await this.fillGroupEntities(msg, ctx);
2432
+ const forward = msg.elements.find((el) => el.multiForwardMsgElement !== void 0)?.multiForwardMsgElement;
2433
+ if (forward !== void 0) ctx.forwardSegments = [incomingForwardSegment(forward.resId, forward.fileName)];
1165
2434
  return toMessageReceiveEvent(msg, ctx);
1166
2435
  }
1167
2436
  /** at uid → uin 批量转换(mention.user_id 用;失败 mention 段跳过)。 */
@@ -1185,30 +2454,34 @@ var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
1185
2454
  ctx.groupEntity = toGroupEntity(detail, msg.peerUid);
1186
2455
  ctx.groupMemberEntity = toGroupMemberEntity(member, msg.peerUid, msg.senderUin);
1187
2456
  }
1188
- /** message_recall 组装(非撤回 grayTip 返回 null 静默)。 */
1189
- async buildRecallEvent(msg) {
1190
- const uidToUin = await this.resolveOperatorUin(msg);
1191
- return toMessageRecallEvent(msg, {
1192
- selfUin: this.api.self.uin,
1193
- ...uidToUin !== void 0 ? { uidToUin } : {}
2457
+ /** 转发 resId → 定位登记(get_forwarded_messages 反查用)。 */
2458
+ recordForward(msg) {
2459
+ const forward = msg.elements.find((el) => el.multiForwardMsgElement !== void 0)?.multiForwardMsgElement;
2460
+ if (forward !== void 0 && forward.resId !== "") this.seqIndex.recordForward(forward.resId, {
2461
+ chatType: msg.chatType,
2462
+ peerUid: msg.peerUid,
2463
+ msgId: msg.msgId
1194
2464
  });
1195
2465
  }
1196
- /** 撤回操作者 uid → uin(未撤回/转换失败返回 undefined,operator_id 兜底 0)。 */
1197
- resolveOperatorUin(msg) {
1198
- const operatorUid = findRevokeOperatorUid(msg);
1199
- if (operatorUid === void 0) return Promise.resolve(void 0);
1200
- return this.api.uidToUin([operatorUid]).catch(() => void 0);
2466
+ /** uid 批量 → uin(空集/失败返回 undefined 语义:调用方用空 Map)。 */
2467
+ resolveUidToUin(uids) {
2468
+ const unique = [...new Set(uids.filter((uid) => uid !== ""))];
2469
+ if (unique.length === 0) return Promise.resolve(void 0);
2470
+ return this.api.uidToUin(unique).catch(() => void 0);
1201
2471
  }
1202
2472
  };
1203
- /** 撤回 grayTip 的操作者 uid(非撤回消息返回 undefined)。 */
1204
- function findRevokeOperatorUid(msg) {
1205
- const uid = msg.elements.find((el) => el.grayTipElement?.revokeElement !== void 0)?.grayTipElement?.revokeElement?.operatorUid;
1206
- return uid === void 0 || uid === "" ? void 0 : uid;
1207
- }
1208
2473
  /** 是否含 grayTip 元素(与 OB11 收链路同判据)。 */
1209
2474
  function hasGrayTip(msg) {
1210
2475
  return msg.elements.some((el) => el.grayTipElement !== void 0);
1211
2476
  }
2477
+ /** 撤回 grayTip 元素(非撤回消息返回 undefined)。 */
2478
+ function findRevokeElement(msg) {
2479
+ return msg.elements.find((el) => el.grayTipElement?.revokeElement !== void 0)?.grayTipElement?.revokeElement;
2480
+ }
2481
+ /** 字符串截断(raw 日志防刷屏)。 */
2482
+ function truncate(raw) {
2483
+ return raw === void 0 ? "" : raw.slice(0, 2e3);
2484
+ }
1212
2485
  //#endregion
1213
2486
  //#region src/milky/config.ts
1214
2487
  /**