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