@napuketto/adapter 0.8.3 → 0.9.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.map +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/core-DW5i42XH.mjs +481 -0
- package/dist/core-DW5i42XH.mjs.map +1 -0
- package/dist/index-h-tsUu0j.d.mts +240 -0
- package/dist/index-h-tsUu0j.d.mts.map +1 -0
- package/dist/kurobot/index.d.mts +1 -1
- package/dist/kurobot/index.mjs +1 -1
- package/dist/milky/index.d.mts +57 -3
- package/dist/milky/index.d.mts.map +1 -1
- package/dist/milky/index.mjs +575 -2
- package/dist/milky/index.mjs.map +1 -1
- package/dist/onebot11/index.d.mts +5 -2
- package/dist/onebot11/index.d.mts.map +1 -1
- package/dist/onebot11/index.mjs +69 -241
- package/dist/onebot11/index.mjs.map +1 -1
- package/dist/satori/index.d.mts +1 -1
- package/dist/satori/index.mjs +1 -1
- package/package.json +2 -2
- package/dist/core-DyXyDUcU.mjs +0 -183
- package/dist/core-DyXyDUcU.mjs.map +0 -1
- package/dist/index-CDKj-jus.d.mts +0 -117
- package/dist/index-CDKj-jus.d.mts.map +0 -1
package/dist/milky/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as BaseProtocolAdapter, b as ActionRegistry, c as nodeAt, d as toHex, f as varintAt, i as extractSysMsgEnvelope, l as recognizeSysMsg, n as decodeProtoTree, p as varintOf, s as narrowSysMsgBlobs, u as textOf } from "../core-DW5i42XH.mjs";
|
|
2
2
|
import { n as firstUntranslatedGrayTip, t as collectGrayTipUids } from "../gray-tip-LwBee5mo.mjs";
|
|
3
3
|
import { n as MS_TO_SEC$2, r as resolveUin, t as narrowBuddyReqs } from "../buddy-req-Q7Q4bVuz.mjs";
|
|
4
|
-
import { n as subscribeRawMessages } from "../raw-message-D_ecJy4C.mjs";
|
|
4
|
+
import { n as subscribeRawMessages, t as forEachRawMessage } from "../raw-message-D_ecJy4C.mjs";
|
|
5
5
|
import { ChatType, GrayTipSubType, GroupApi, GroupNotifyMsgStatus, GroupNotifyMsgType, NTGroupRequestOperateTypes, TicketApi, TipGroupElementType, isKernelError, kernelError, toCanonicalElements } from "@napuketto/kernel";
|
|
6
6
|
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
7
7
|
import { detectFileType, downloadUrl, inferExtension } from "@napuketto/media";
|
|
@@ -2250,6 +2250,103 @@ function createMilkyActionRegistry(opts) {
|
|
|
2250
2250
|
return registry;
|
|
2251
2251
|
}
|
|
2252
2252
|
//#endregion
|
|
2253
|
+
//#region src/milky/helper/essence.ts
|
|
2254
|
+
/** 单个候选值 → 群号字符串(非空字符串 / 正整数)。 */
|
|
2255
|
+
function toGroupCode(value) {
|
|
2256
|
+
if (typeof value === "string" && value !== "") return value;
|
|
2257
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return String(value);
|
|
2258
|
+
return null;
|
|
2259
|
+
}
|
|
2260
|
+
/** onGroupEssenceListChange 参数 → 群号(录制实证首项字符串;防御收窄)。 */
|
|
2261
|
+
function narrowEssenceGroupArg(arg) {
|
|
2262
|
+
const first = Array.isArray(arg) ? arg[0] : arg;
|
|
2263
|
+
if (first !== null && typeof first === "object") {
|
|
2264
|
+
const rec = first;
|
|
2265
|
+
for (const key of [
|
|
2266
|
+
"groupCode",
|
|
2267
|
+
"group_code",
|
|
2268
|
+
"groupId",
|
|
2269
|
+
"peerUid"
|
|
2270
|
+
]) {
|
|
2271
|
+
const code = toGroupCode(rec[key]);
|
|
2272
|
+
if (code !== null) return code;
|
|
2273
|
+
}
|
|
2274
|
+
return null;
|
|
2275
|
+
}
|
|
2276
|
+
return toGroupCode(first);
|
|
2277
|
+
}
|
|
2278
|
+
/** 精华列表项 → 缓存操作者(add_digest_uin 缺失/空 → 空串 = 未知)。 */
|
|
2279
|
+
function essenceOperator(item) {
|
|
2280
|
+
return typeof item.add_digest_uin === "string" && item.add_digest_uin !== "" ? item.add_digest_uin : "";
|
|
2281
|
+
}
|
|
2282
|
+
/** 列表 → 缓存 map(seq → 操作者)。 */
|
|
2283
|
+
function buildEssenceCache(items) {
|
|
2284
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2285
|
+
for (const item of items) if (typeof item.msg_seq === "number") cache.set(item.msg_seq, essenceOperator(item));
|
|
2286
|
+
return cache;
|
|
2287
|
+
}
|
|
2288
|
+
/** 新增项收集(cache 新键即新增)。 */
|
|
2289
|
+
function collectAdded(prev, cache) {
|
|
2290
|
+
const added = [];
|
|
2291
|
+
for (const [seq, operator] of cache) if (!prev.has(seq)) added.push({
|
|
2292
|
+
messageSeq: seq,
|
|
2293
|
+
operatorUin: operator === "" ? null : operator
|
|
2294
|
+
});
|
|
2295
|
+
return added;
|
|
2296
|
+
}
|
|
2297
|
+
/** 移除项收集(cache 无即移除;移除者不可知)。 */
|
|
2298
|
+
function collectRemoved(prev, cache) {
|
|
2299
|
+
const removed = [];
|
|
2300
|
+
for (const seq of prev.keys()) if (!cache.has(seq)) removed.push(seq);
|
|
2301
|
+
return removed;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* 精华列表 diff:prev 缺省(基线未建立)时只建缓存不产出事件——
|
|
2305
|
+
* 首个变更事件只做基线,避免全量首推被误报为「逐条加精」。
|
|
2306
|
+
*/
|
|
2307
|
+
function diffEssenceList(prev, items) {
|
|
2308
|
+
const cache = buildEssenceCache(items);
|
|
2309
|
+
if (prev === void 0) return {
|
|
2310
|
+
cache,
|
|
2311
|
+
added: [],
|
|
2312
|
+
removed: []
|
|
2313
|
+
};
|
|
2314
|
+
return {
|
|
2315
|
+
cache,
|
|
2316
|
+
added: collectAdded(prev, cache),
|
|
2317
|
+
removed: collectRemoved(prev, cache)
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
//#endregion
|
|
2321
|
+
//#region src/milky/helper/file-upload.ts
|
|
2322
|
+
/**
|
|
2323
|
+
* friend_file_upload 翻译(Milky 批次三,2026-11-01 录制定型)
|
|
2324
|
+
*
|
|
2325
|
+
* 源:Msg/onRecvOfflineFileMsg 与 Msg/onFileMsgCome(同载荷 RawMessage 数组,
|
|
2326
|
+
* 双回调按 msgId 在适配器去重);fileElement{fileName, fileSize 字符串,
|
|
2327
|
+
* file10MMd5, fileUuid, ...},sendType 0=收到(sender=对端)/ 2=自己发出。
|
|
2328
|
+
*
|
|
2329
|
+
* 已知差异(milky.md §0.8.6):官方 file_hash 语义为 TriSHA1,接收侧
|
|
2330
|
+
* fileSha/fileSha3 实测为空——取 file10MMd5(md5)充值,上传侧校准后回填。
|
|
2331
|
+
*/
|
|
2332
|
+
/** 单条 C2C 文件消息 → 事件载荷(非 C2C / 无 fileElement / 无 fileUuid → null)。 */
|
|
2333
|
+
function toFriendFileUploadData(msg, selfUin, uidToUin) {
|
|
2334
|
+
if (msg.chatType !== ChatType.C2C) return null;
|
|
2335
|
+
const file = msg.elements.find((el) => el.fileElement !== void 0)?.fileElement;
|
|
2336
|
+
if (file === void 0 || typeof file.fileUuid !== "string" || file.fileUuid === "") return null;
|
|
2337
|
+
const isSelf = msg.sendType === 2 || String(msg.senderUin) === selfUin;
|
|
2338
|
+
const userUin = msg.senderUin !== "" ? Number(msg.senderUin) : resolveUin(msg.senderUid, uidToUin);
|
|
2339
|
+
const size = Number(file.fileSize ?? 0);
|
|
2340
|
+
return {
|
|
2341
|
+
user_id: Number.isFinite(userUin) ? userUin : 0,
|
|
2342
|
+
file_id: file.fileUuid,
|
|
2343
|
+
file_name: file.fileName ?? "",
|
|
2344
|
+
file_size: Number.isFinite(size) ? size : 0,
|
|
2345
|
+
file_hash: file.file10MMd5 ?? file.fileSha ?? "",
|
|
2346
|
+
is_self: isSelf
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
//#endregion
|
|
2253
2350
|
//#region src/milky/helper/notice.ts
|
|
2254
2351
|
/** 毫秒 → 秒(Unix 时间戳)。 */
|
|
2255
2352
|
const MS_TO_SEC = 1e3;
|
|
@@ -2396,6 +2493,94 @@ function toMilkyNoticeEvent(msg, ctx) {
|
|
|
2396
2493
|
return null;
|
|
2397
2494
|
}
|
|
2398
2495
|
//#endregion
|
|
2496
|
+
//#region src/milky/helper/peer-pin.ts
|
|
2497
|
+
/**
|
|
2498
|
+
* peer_pin_change 翻译(Milky 批次三,RecentContact/onRecentContactListChangedVer2)
|
|
2499
|
+
*
|
|
2500
|
+
* 回调随消息活动高频快照(含 topFlag 的全量条目 + chatType=7 影子条目),
|
|
2501
|
+
* 不能见条目就广播——按「同会话 topFlag 前后值翻转」检测(PinWatch)。
|
|
2502
|
+
* 启动期以 getPinnedContacts 播种基线(重启后首个取消置顶也能观测到)。
|
|
2503
|
+
*
|
|
2504
|
+
* 证据:2026-11-01 set_peer_pin 自触发翻转实验——changedList 条目
|
|
2505
|
+
* {chatType, peerUin, topFlag(1/0), topFlagTime(秒级 unix 字符串)},与操作
|
|
2506
|
+
* 一一对应(milky.md §0.8.6)。
|
|
2507
|
+
*/
|
|
2508
|
+
/** uin 形判别(peer_id 必须是数字串)。 */
|
|
2509
|
+
const NUMERIC_RE$2 = /^\d+$/;
|
|
2510
|
+
/** 置顶翻转检测(每适配器实例一份,键 = chatType:peerUin——群号与 uin 数值空间可重叠)。 */
|
|
2511
|
+
var PinWatch = class {
|
|
2512
|
+
states = /* @__PURE__ */ new Map();
|
|
2513
|
+
/** 启动基线:置顶会话播种(只记状态,不产生事件)。 */
|
|
2514
|
+
seed(pins) {
|
|
2515
|
+
for (const pin of pins) {
|
|
2516
|
+
if (pin.peerUin === void 0 || pin.peerUin === "") continue;
|
|
2517
|
+
const chatType = pin.scene === "group" ? ChatType.GROUP : ChatType.C2C;
|
|
2518
|
+
this.states.set(`${chatType}:${pin.peerUin}`, 1);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* 观测一个变更条目:topFlag 相对上次观测翻转 → true(应广播);
|
|
2523
|
+
* 首见(基线未建立)或未变 → false。
|
|
2524
|
+
*/
|
|
2525
|
+
observe(chatType, peerUin, topFlag) {
|
|
2526
|
+
const key = `${chatType}:${peerUin}`;
|
|
2527
|
+
const prev = this.states.get(key);
|
|
2528
|
+
this.states.set(key, topFlag);
|
|
2529
|
+
return prev !== void 0 && prev !== topFlag;
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
/** 场景映射(changedList 条目 chatType → Milky message_scene;影子/其他类型丢弃)。 */
|
|
2533
|
+
function pinScene(chatType) {
|
|
2534
|
+
if (chatType === ChatType.GROUP) return "group";
|
|
2535
|
+
if (chatType === ChatType.C2C) return "friend";
|
|
2536
|
+
return null;
|
|
2537
|
+
}
|
|
2538
|
+
/** 变更条目 → 事件载荷(null = 条目不含置顶字段/场景不支持,跳过不广播)。 */
|
|
2539
|
+
function toPeerPinChangeData(entry) {
|
|
2540
|
+
const scene = pinScene(entry.chatType);
|
|
2541
|
+
if (scene === null || entry.topFlag === void 0) return null;
|
|
2542
|
+
const peerUin = typeof entry.peerUin === "string" && entry.peerUin !== "" ? entry.peerUin : scene === "group" ? entry.peerUid : "";
|
|
2543
|
+
if (peerUin === "" || !NUMERIC_RE$2.test(peerUin)) return null;
|
|
2544
|
+
return {
|
|
2545
|
+
data: {
|
|
2546
|
+
message_scene: scene,
|
|
2547
|
+
peer_id: Number(peerUin),
|
|
2548
|
+
is_pinned: entry.topFlag === 1
|
|
2549
|
+
},
|
|
2550
|
+
topFlagTime: typeof entry.topFlagTime === "string" ? entry.topFlagTime : void 0,
|
|
2551
|
+
peerUin
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
//#endregion
|
|
2555
|
+
//#region src/milky/helper/recent-keys.ts
|
|
2556
|
+
/**
|
|
2557
|
+
* 带过期去重键缓存(Milky 批次三)
|
|
2558
|
+
*
|
|
2559
|
+
* 两个场景共用:friend_file_upload 双回调(onRecvOfflineFileMsg / onFileMsgCome
|
|
2560
|
+
* 同载荷,按 msgId 去重)与 group_essence_message_change 双路径(732:1 快路径
|
|
2561
|
+
* vs 列表 diff,按 群:seq:方向 去重)。claim 语义 = 未登记过(或已过期)则登记
|
|
2562
|
+
* 并返回 true——调用方据此决定是否广播。
|
|
2563
|
+
*/
|
|
2564
|
+
var RecentKeyCache = class {
|
|
2565
|
+
entries = /* @__PURE__ */ new Map();
|
|
2566
|
+
ttlMs;
|
|
2567
|
+
constructor(ttlMs) {
|
|
2568
|
+
this.ttlMs = ttlMs;
|
|
2569
|
+
}
|
|
2570
|
+
/** 未命中则登记并返回 true(应广播);命中(窗口内重复)返回 false。 */
|
|
2571
|
+
claim(key, now = Date.now()) {
|
|
2572
|
+
this.prune(now);
|
|
2573
|
+
const expiry = this.entries.get(key);
|
|
2574
|
+
if (expiry !== void 0 && expiry > now) return false;
|
|
2575
|
+
this.entries.set(key, now + this.ttlMs);
|
|
2576
|
+
return true;
|
|
2577
|
+
}
|
|
2578
|
+
/** 惰性清理过期键(每次 claim 顺带执行,量级小无需定时器)。 */
|
|
2579
|
+
prune(now) {
|
|
2580
|
+
for (const [key, expiry] of this.entries) if (expiry <= now) this.entries.delete(key);
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
//#endregion
|
|
2399
2584
|
//#region src/milky/helper/seq-index.ts
|
|
2400
2585
|
/** 单方向最大条目数(测试用例引用容量语义)。 */
|
|
2401
2586
|
const MAX_ENTRIES = 4096;
|
|
@@ -2474,6 +2659,102 @@ var MessageSeqIndex = class {
|
|
|
2474
2659
|
}
|
|
2475
2660
|
};
|
|
2476
2661
|
//#endregion
|
|
2662
|
+
//#region src/milky/helper/sysmsg.ts
|
|
2663
|
+
/**
|
|
2664
|
+
* Milky sysmsg 识别表(批次三,2026-11-01;识别机制共享自 core/sysmsg.ts)
|
|
2665
|
+
*
|
|
2666
|
+
* 键 = 原生分发键 "msgType:subType",规则产出事件 data 体(信封 time/self_id/
|
|
2667
|
+
* event_type 由适配器统一包装)。路径依据 = 三轮录制 wire bytes 全量解码
|
|
2668
|
+
* (milky.md §0.8.6 识别表;临时解码脚本 .napuketto/decode-sysmsg*.mjs 不入库)。
|
|
2669
|
+
*
|
|
2670
|
+
* - 732:16 群消息表情回应:f1.f1=群号;f3.f2.f44.f1.f1={f1 变动值, f2.f1=msgSeq,
|
|
2671
|
+
* f3={f1 faceId 字符串, f5: 1=添加/2=撤销, f3:1 仅添加时存在}}。
|
|
2672
|
+
* operator 不在 push(f4=bot 自身 uid 占位)——user_id 省略。
|
|
2673
|
+
* - 732:1 群精华设置:f1.f1=群号, f1.f8.f8.f1=msgSeq;取消精华无独立 sysmsg
|
|
2674
|
+
* (remove 走 onGroupEssenceListChange 列表 diff,helper/essence.ts)。
|
|
2675
|
+
* - 528:349/381/382 多端设备同步族:不广播。
|
|
2676
|
+
*/
|
|
2677
|
+
/** 数字串判别(faceId 实证为数字字符串)。 */
|
|
2678
|
+
const NUMERIC_RE$1 = /^\d+$/;
|
|
2679
|
+
/** 732:16 → group_message_reaction。 */
|
|
2680
|
+
const REACTION_RULE = {
|
|
2681
|
+
kind: "group_message_reaction",
|
|
2682
|
+
extract: (tree) => reactionData(tree)
|
|
2683
|
+
};
|
|
2684
|
+
/** 路径提取:f1.f1=群号 + f3.f2.f44.f1.f1=回应明细(任一缺失 → null)。 */
|
|
2685
|
+
function reactionData(tree) {
|
|
2686
|
+
const groupId = varintAt(tree, [1, 1]);
|
|
2687
|
+
const detail = nodeAt(tree, [
|
|
2688
|
+
3,
|
|
2689
|
+
2,
|
|
2690
|
+
44,
|
|
2691
|
+
1,
|
|
2692
|
+
1
|
|
2693
|
+
]);
|
|
2694
|
+
if (groupId === null || detail === null) return null;
|
|
2695
|
+
return reactionDetail(groupId, detail);
|
|
2696
|
+
}
|
|
2697
|
+
/** 明细提取:f2.f1=msgSeq + f3=face 块(任一缺失 → null)。 */
|
|
2698
|
+
function reactionDetail(groupId, detail) {
|
|
2699
|
+
const msgSeq = varintAt(detail, [2, 1]);
|
|
2700
|
+
const face = nodeAt(detail, [3]);
|
|
2701
|
+
if (msgSeq === null || face === null) return null;
|
|
2702
|
+
return reactionFace(groupId, msgSeq, face);
|
|
2703
|
+
}
|
|
2704
|
+
/** face 块提取:faceId 实证为数字字符串("76"/"124"…);f5 双判据(f3 仅添加时存在)。 */
|
|
2705
|
+
function reactionFace(groupId, msgSeq, face) {
|
|
2706
|
+
const isAdd = reactionIsAdd(face);
|
|
2707
|
+
const faceId = reactionFaceId(face);
|
|
2708
|
+
if (isAdd === null || faceId === null) return null;
|
|
2709
|
+
return {
|
|
2710
|
+
group_id: groupId,
|
|
2711
|
+
message_seq: msgSeq,
|
|
2712
|
+
face_id: faceId,
|
|
2713
|
+
reaction_type: "face",
|
|
2714
|
+
is_add: isAdd
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
/** faceId 字段(数字字符串 → number;缺失/非数字 → null)。 */
|
|
2718
|
+
function reactionFaceId(face) {
|
|
2719
|
+
const faceIdText = textOf(face, 1);
|
|
2720
|
+
return faceIdText !== null && NUMERIC_RE$1.test(faceIdText) ? Number(faceIdText) : null;
|
|
2721
|
+
}
|
|
2722
|
+
/** is_add 判定:f5=1 添加 / 2 撤销(f3=1 仅添加时存在,双判据);非法 → null。 */
|
|
2723
|
+
function reactionIsAdd(face) {
|
|
2724
|
+
const op = varintOf(face, 5);
|
|
2725
|
+
if (op === 1) return true;
|
|
2726
|
+
return op === 2 ? false : null;
|
|
2727
|
+
}
|
|
2728
|
+
/** (msgType, subType) → 规则表(键 = 原生分发键)。 */
|
|
2729
|
+
const KIND_TABLE = /* @__PURE__ */ new Map([["732:16", REACTION_RULE], ["732:1", {
|
|
2730
|
+
kind: "group_essence_message_change",
|
|
2731
|
+
extract: (tree) => {
|
|
2732
|
+
const groupId = varintAt(tree, [1, 1]);
|
|
2733
|
+
const msgSeq = varintAt(tree, [
|
|
2734
|
+
1,
|
|
2735
|
+
8,
|
|
2736
|
+
8,
|
|
2737
|
+
1
|
|
2738
|
+
]);
|
|
2739
|
+
if (groupId === null || msgSeq === null) return null;
|
|
2740
|
+
return {
|
|
2741
|
+
group_id: groupId,
|
|
2742
|
+
message_seq: msgSeq,
|
|
2743
|
+
is_set: true
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
}]]);
|
|
2747
|
+
/** 已观测但未命名的键(多端设备同步族等,不广播只走校准日志)。 */
|
|
2748
|
+
const OBSERVED_UNNAMED = /* @__PURE__ */ new Set([
|
|
2749
|
+
"528:382",
|
|
2750
|
+
"528:349",
|
|
2751
|
+
"528:381"
|
|
2752
|
+
]);
|
|
2753
|
+
/** (msgType, subType) → 识别结果(绑定 Milky 表;table 参数供单测注入)。 */
|
|
2754
|
+
function recognizeMilkySysMsg(msgType, subType, table = KIND_TABLE) {
|
|
2755
|
+
return recognizeSysMsg(msgType, subType, table, OBSERVED_UNNAMED);
|
|
2756
|
+
}
|
|
2757
|
+
//#endregion
|
|
2477
2758
|
//#region src/milky/transport.ts
|
|
2478
2759
|
/** /event 路径(规范固定)。 */
|
|
2479
2760
|
const EVENT_PATH = "/event";
|
|
@@ -2648,6 +2929,18 @@ var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
|
|
|
2648
2929
|
msgChannel;
|
|
2649
2930
|
groupChannel;
|
|
2650
2931
|
buddyChannel;
|
|
2932
|
+
recentContactChannel;
|
|
2933
|
+
loginChannel;
|
|
2934
|
+
/** peer_pin 翻转检测(批次三;启动期 getPinnedContacts 播种)。 */
|
|
2935
|
+
pinWatch = new PinWatch();
|
|
2936
|
+
/** friend_file_upload 双回调去重(同载荷,按 msgId)。 */
|
|
2937
|
+
fileKeys = new RecentKeyCache(6e5);
|
|
2938
|
+
/** essence 双路径去重(732:1 快路径 vs 列表 diff,键 群:seq:方向)。 */
|
|
2939
|
+
essenceKeys = new RecentKeyCache(3e5);
|
|
2940
|
+
/** 精华列表缓存(群号 → seq → 操作者 uin,空串 = 未知)。 */
|
|
2941
|
+
essenceCaches = /* @__PURE__ */ new Map();
|
|
2942
|
+
/** 精华列表拉取串行链(并发回写乱序会把新列表覆盖成旧列表)。 */
|
|
2943
|
+
essenceChain = Promise.resolve();
|
|
2651
2944
|
calibLogger;
|
|
2652
2945
|
api;
|
|
2653
2946
|
seqIndex = new MessageSeqIndex();
|
|
@@ -2668,6 +2961,8 @@ var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
|
|
|
2668
2961
|
this.msgChannel = opts.msgChannel;
|
|
2669
2962
|
this.groupChannel = opts.groupChannel;
|
|
2670
2963
|
this.buddyChannel = opts.buddyChannel;
|
|
2964
|
+
this.recentContactChannel = opts.recentContactChannel;
|
|
2965
|
+
this.loginChannel = opts.loginChannel;
|
|
2671
2966
|
this.calibLogger = opts.logger;
|
|
2672
2967
|
this.api = new MilkyApi(opts, this.seqIndex);
|
|
2673
2968
|
this.registry = createMilkyActionRegistry({ api: this.api });
|
|
@@ -2687,6 +2982,7 @@ var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
|
|
|
2687
2982
|
...this.calibLogger !== void 0 ? { logger: this.calibLogger } : {}
|
|
2688
2983
|
});
|
|
2689
2984
|
this.stopTransports = set.close;
|
|
2985
|
+
await Promise.all(set.adapters.map((a) => a.open()));
|
|
2690
2986
|
}
|
|
2691
2987
|
this.subscribe();
|
|
2692
2988
|
this.calibLogger?.info({
|
|
@@ -2714,6 +3010,19 @@ var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
|
|
|
2714
3010
|
this.unsubscribes.push(subscribeRawMessages(this.msgChannel, (msg) => {
|
|
2715
3011
|
this.handleIncoming(msg);
|
|
2716
3012
|
}));
|
|
3013
|
+
this.unsubscribes.push(this.msgChannel.on("Msg/onRecvSysMsg", (arg) => {
|
|
3014
|
+
this.handleSysMsg(arg);
|
|
3015
|
+
}));
|
|
3016
|
+
this.unsubscribes.push(this.msgChannel.on("Msg/onRecvOfflineFileMsg", (msgs) => {
|
|
3017
|
+
this.handleFileMessages(msgs);
|
|
3018
|
+
}), this.msgChannel.on("Msg/onFileMsgCome", (msgs) => {
|
|
3019
|
+
this.handleFileMessages(msgs);
|
|
3020
|
+
}));
|
|
3021
|
+
this.subscribeGroupAndBuddy();
|
|
3022
|
+
this.subscribeBatch3();
|
|
3023
|
+
}
|
|
3024
|
+
/** 群通知 + 好友请求通道(P1 事件源;通道缺省不订阅)。 */
|
|
3025
|
+
subscribeGroupAndBuddy() {
|
|
2717
3026
|
if (this.groupChannel !== void 0) {
|
|
2718
3027
|
const groupChannel = this.groupChannel;
|
|
2719
3028
|
this.unsubscribes.push(groupChannel.on("Group/onGroupNotifiesUpdated", (doubt, notifies) => {
|
|
@@ -2727,6 +3036,30 @@ var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
|
|
|
2727
3036
|
}));
|
|
2728
3037
|
}
|
|
2729
3038
|
}
|
|
3039
|
+
/** 批次三通道:精华列表变更 / 置顶翻转(含播种基线)/ bot_offline。 */
|
|
3040
|
+
subscribeBatch3() {
|
|
3041
|
+
if (this.groupChannel !== void 0) {
|
|
3042
|
+
const groupChannel = this.groupChannel;
|
|
3043
|
+
this.unsubscribes.push(groupChannel.on("Group/onGroupEssenceListChange", (arg) => {
|
|
3044
|
+
this.handleEssenceChange(arg);
|
|
3045
|
+
}));
|
|
3046
|
+
}
|
|
3047
|
+
if (this.recentContactChannel !== void 0) {
|
|
3048
|
+
const recentContactChannel = this.recentContactChannel;
|
|
3049
|
+
this.unsubscribes.push(recentContactChannel.on("RecentContact/onRecentContactListChangedVer2", (lists) => {
|
|
3050
|
+
this.handlePinChanges(lists);
|
|
3051
|
+
}));
|
|
3052
|
+
this.seedPinWatch();
|
|
3053
|
+
}
|
|
3054
|
+
if (this.loginChannel !== void 0) {
|
|
3055
|
+
const loginChannel = this.loginChannel;
|
|
3056
|
+
this.unsubscribes.push(loginChannel.on("Login/onLoginDisConnected", (...args) => {
|
|
3057
|
+
this.broadcastBotOffline("disconnected", args);
|
|
3058
|
+
}), loginChannel.on("Login/onLogoutSucceed", (...args) => {
|
|
3059
|
+
this.broadcastBotOffline("logout", args);
|
|
3060
|
+
}));
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
2730
3063
|
/** 退订(幂等)。 */
|
|
2731
3064
|
unsubscribeAll() {
|
|
2732
3065
|
for (const off of this.unsubscribes) off();
|
|
@@ -2834,6 +3167,200 @@ var NapukettoMilkyAdapter = class extends BaseProtocolAdapter {
|
|
|
2834
3167
|
msgId: msg.msgId
|
|
2835
3168
|
});
|
|
2836
3169
|
}
|
|
3170
|
+
/**
|
|
3171
|
+
* sysmsg 总闸:解码 → 识别 → 表命中才广播(732:16 表情回应 / 732:1 精华
|
|
3172
|
+
* 设置);未识别 debug 校准日志。规则提取失败不广播半成品事件。
|
|
3173
|
+
*/
|
|
3174
|
+
handleSysMsg(arg) {
|
|
3175
|
+
const blobs = narrowSysMsgBlobs(arg);
|
|
3176
|
+
if (blobs === null) {
|
|
3177
|
+
this.calibLogger?.warn({ raw: truncate(JSON.stringify(arg)) }, "milky: onRecvSysMsg 未知参数形状");
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
for (const blob of blobs) this.handleSysMsgBlob(blob);
|
|
3181
|
+
}
|
|
3182
|
+
/** 单条字节块:解码 → 识别 → 提取;任一步失败走校准日志返回 null。 */
|
|
3183
|
+
handleSysMsgBlob(blob) {
|
|
3184
|
+
const hit = this.sysMsgHit(blob);
|
|
3185
|
+
if (hit === null) return;
|
|
3186
|
+
if (!this.claimEssenceFastPath(hit)) return;
|
|
3187
|
+
this.broadcastEvent({
|
|
3188
|
+
time: hit.env.time ?? Math.floor(Date.now() / 1e3),
|
|
3189
|
+
self_id: Number(this.api.self.uin),
|
|
3190
|
+
event_type: hit.rule.kind,
|
|
3191
|
+
data: hit.data
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
/** 解码 + 识别 + 提取(每步失败各自打 debug 校准日志)。 */
|
|
3195
|
+
sysMsgHit(blob) {
|
|
3196
|
+
const tree = decodeProtoTree(blob);
|
|
3197
|
+
if (tree === null) {
|
|
3198
|
+
this.calibLogger?.debug({ raw: toHex(blob).slice(0, 1024) }, "milky: onRecvSysMsg protobuf 解码失败");
|
|
3199
|
+
return null;
|
|
3200
|
+
}
|
|
3201
|
+
const env = extractSysMsgEnvelope(tree);
|
|
3202
|
+
const rule = this.sysMsgRule(env, tree, blob);
|
|
3203
|
+
if (rule === null) return null;
|
|
3204
|
+
return {
|
|
3205
|
+
env,
|
|
3206
|
+
rule: rule.rule,
|
|
3207
|
+
data: rule.data
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
/** 识别 + 规则提取(未识别/提取失败 → 校准日志 + null)。 */
|
|
3211
|
+
sysMsgRule(env, tree, blob) {
|
|
3212
|
+
const rec = recognizeMilkySysMsg(env.msgType, env.subType);
|
|
3213
|
+
if (rec.kind !== "notice") {
|
|
3214
|
+
this.calibLogger?.debug({
|
|
3215
|
+
verdict: rec.kind,
|
|
3216
|
+
msgType: env.msgType,
|
|
3217
|
+
subType: env.subType,
|
|
3218
|
+
strings: env.strings,
|
|
3219
|
+
raw: toHex(blob).slice(0, 1024)
|
|
3220
|
+
}, "milky: onRecvSysMsg 校准数据(未识别,不广播)");
|
|
3221
|
+
return null;
|
|
3222
|
+
}
|
|
3223
|
+
const ctx = {
|
|
3224
|
+
selfUin: Number(this.api.self.uin),
|
|
3225
|
+
time: env.time ?? Math.floor(Date.now() / 1e3)
|
|
3226
|
+
};
|
|
3227
|
+
const data = rec.rule.extract(tree, env, () => 0, ctx);
|
|
3228
|
+
if (data === null) {
|
|
3229
|
+
this.calibLogger?.debug({
|
|
3230
|
+
kind: rec.rule.kind,
|
|
3231
|
+
msgType: env.msgType,
|
|
3232
|
+
subType: env.subType
|
|
3233
|
+
}, "milky: sysmsg 规则提取失败(不广播)");
|
|
3234
|
+
return null;
|
|
3235
|
+
}
|
|
3236
|
+
return {
|
|
3237
|
+
rule: rec.rule,
|
|
3238
|
+
data
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
/** 精华快路径去重守卫:非精华事件恒 true;精华事件 claim 失败(已由列表 diff 抢发)→ false。 */
|
|
3242
|
+
claimEssenceFastPath(hit) {
|
|
3243
|
+
if (hit.rule.kind !== "group_essence_message_change") return true;
|
|
3244
|
+
const essence = hit.data;
|
|
3245
|
+
return this.essenceKeys.claim(`${essence.group_id}:${essence.message_seq}:1`);
|
|
3246
|
+
}
|
|
3247
|
+
/** 私聊文件消息 → friend_file_upload(双回调同载荷,按 msgId 去重)。 */
|
|
3248
|
+
handleFileMessages(msgs) {
|
|
3249
|
+
forEachRawMessage(msgs, (msg) => {
|
|
3250
|
+
const data = toFriendFileUploadData(msg, this.api.self.uin, /* @__PURE__ */ new Map());
|
|
3251
|
+
if (data === null) return;
|
|
3252
|
+
if (data.user_id === 0) this.calibLogger?.debug({
|
|
3253
|
+
msgId: msg.msgId,
|
|
3254
|
+
senderUid: msg.senderUid
|
|
3255
|
+
}, "milky: friend_file_upload senderUin 缺失(user_id=0)");
|
|
3256
|
+
if (!this.fileKeys.claim(`file:${msg.msgId}`)) return;
|
|
3257
|
+
this.broadcastEvent({
|
|
3258
|
+
time: secondsOf(msg.msgTime),
|
|
3259
|
+
self_id: Number(this.api.self.uin),
|
|
3260
|
+
event_type: "friend_file_upload",
|
|
3261
|
+
data
|
|
3262
|
+
});
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
/** 精华列表变更:拉全量列表 diff(remove 路径 + operator 回填;串行防乱序)。 */
|
|
3266
|
+
handleEssenceChange(arg) {
|
|
3267
|
+
const groupCode = narrowEssenceGroupArg(arg);
|
|
3268
|
+
if (groupCode === null) {
|
|
3269
|
+
this.calibLogger?.warn({ raw: truncate(JSON.stringify(arg)) }, "milky: onGroupEssenceListChange 未知参数形状");
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
this.essenceChain = this.essenceChain.then(() => this.syncEssenceList(groupCode), () => void 0);
|
|
3273
|
+
}
|
|
3274
|
+
/** 单群精华列表同步:拉取 → diff → 广播 → 更新缓存(基线未建立只建基线)。 */
|
|
3275
|
+
async syncEssenceList(groupCode) {
|
|
3276
|
+
let items;
|
|
3277
|
+
try {
|
|
3278
|
+
items = await this.api.webApi.getEssenceMsgList(groupCode);
|
|
3279
|
+
} catch (e) {
|
|
3280
|
+
this.calibLogger?.warn({
|
|
3281
|
+
group: groupCode,
|
|
3282
|
+
err: String(e)
|
|
3283
|
+
}, "milky: 精华列表拉取失败(diff 跳过)");
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
const prev = this.essenceCaches.get(groupCode);
|
|
3287
|
+
const { cache, added, removed } = diffEssenceList(prev, items);
|
|
3288
|
+
this.essenceCaches.set(groupCode, cache);
|
|
3289
|
+
if (prev === void 0) return;
|
|
3290
|
+
const selfId = Number(this.api.self.uin);
|
|
3291
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3292
|
+
this.broadcastEssenceAdded(groupCode, added, now, selfId);
|
|
3293
|
+
this.broadcastEssenceRemoved(groupCode, removed, now, selfId);
|
|
3294
|
+
}
|
|
3295
|
+
/** diff 新增项广播(is_set=true;operator = add_digest_uin;快路径已发则跳过)。 */
|
|
3296
|
+
broadcastEssenceAdded(groupCode, added, now, selfId) {
|
|
3297
|
+
for (const item of added) {
|
|
3298
|
+
if (!this.essenceKeys.claim(`${groupCode}:${item.messageSeq}:1`)) continue;
|
|
3299
|
+
const data = essenceAddedData(groupCode, item);
|
|
3300
|
+
this.broadcastEvent({
|
|
3301
|
+
time: now,
|
|
3302
|
+
self_id: selfId,
|
|
3303
|
+
event_type: "group_essence_message_change",
|
|
3304
|
+
data
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
/** diff 移除项广播(is_set=false;取消无 sysmsg,operator 不可得省略)。 */
|
|
3309
|
+
broadcastEssenceRemoved(groupCode, removed, now, selfId) {
|
|
3310
|
+
for (const seq of removed) {
|
|
3311
|
+
if (!this.essenceKeys.claim(`${groupCode}:${seq}:0`)) continue;
|
|
3312
|
+
this.broadcastEvent({
|
|
3313
|
+
time: now,
|
|
3314
|
+
self_id: selfId,
|
|
3315
|
+
event_type: "group_essence_message_change",
|
|
3316
|
+
data: {
|
|
3317
|
+
group_id: Number(groupCode),
|
|
3318
|
+
message_seq: seq,
|
|
3319
|
+
is_set: false
|
|
3320
|
+
}
|
|
3321
|
+
});
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
/** 会话列表变更 → peer_pin_change(topFlag 翻转检测,条目自带 chatType 判场景)。 */
|
|
3325
|
+
handlePinChanges(lists) {
|
|
3326
|
+
const arr = Array.isArray(lists) ? lists : [lists];
|
|
3327
|
+
for (const list of arr) this.handlePinChangeList(list);
|
|
3328
|
+
}
|
|
3329
|
+
/** 单个变更列表:changedList 逐条目翻转检测 + 广播。 */
|
|
3330
|
+
handlePinChangeList(list) {
|
|
3331
|
+
for (const entry of changedListOf(list)) this.handlePinEntry(entry);
|
|
3332
|
+
}
|
|
3333
|
+
/** 单个变更条目:翻译 + 翻转检测 + 广播(首见/未变/不支持场景静默跳过)。 */
|
|
3334
|
+
handlePinEntry(entry) {
|
|
3335
|
+
const payload = pinPayloadOf(entry);
|
|
3336
|
+
if (payload === null) return;
|
|
3337
|
+
if (!this.pinWatch.observe(payload.chatType, payload.peerUin, payload.topFlag)) return;
|
|
3338
|
+
this.broadcastEvent({
|
|
3339
|
+
time: secondsOf(payload.topFlagTime),
|
|
3340
|
+
self_id: Number(this.api.self.uin),
|
|
3341
|
+
event_type: "peer_pin_change",
|
|
3342
|
+
data: payload.data
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
/** 启动基线:置顶会话播种(重启后首个取消置顶也能观测到翻转;失败静默)。 */
|
|
3346
|
+
seedPinWatch() {
|
|
3347
|
+
this.api.recentContactApi.getPinnedContacts().then((pins) => {
|
|
3348
|
+
this.pinWatch.seed(pins);
|
|
3349
|
+
}).catch(() => void 0);
|
|
3350
|
+
}
|
|
3351
|
+
/** bot_offline:Login 桥回调(载荷零样本,reason 固定值 + raw 校准日志待野捕)。 */
|
|
3352
|
+
broadcastBotOffline(reason, args) {
|
|
3353
|
+
this.calibLogger?.debug({
|
|
3354
|
+
reason,
|
|
3355
|
+
raw: truncate(JSON.stringify(args))
|
|
3356
|
+
}, "milky: bot_offline(reason 为固定值,回调载荷待野捕校准)");
|
|
3357
|
+
this.broadcastEvent({
|
|
3358
|
+
time: Math.floor(Date.now() / 1e3),
|
|
3359
|
+
self_id: Number(this.api.self.uin),
|
|
3360
|
+
event_type: "bot_offline",
|
|
3361
|
+
data: { reason }
|
|
3362
|
+
});
|
|
3363
|
+
}
|
|
2837
3364
|
/** uid 批量 → uin(空集/失败返回 undefined 语义:调用方用空 Map)。 */
|
|
2838
3365
|
resolveUidToUin(uids) {
|
|
2839
3366
|
const unique = [...new Set(uids.filter((uid) => uid !== ""))];
|
|
@@ -2853,6 +3380,52 @@ function findRevokeElement(msg) {
|
|
|
2853
3380
|
function truncate(raw) {
|
|
2854
3381
|
return raw === void 0 ? "" : raw.slice(0, 2e3);
|
|
2855
3382
|
}
|
|
3383
|
+
/** 数字串判别(uin / 秒级时间戳)。 */
|
|
3384
|
+
const NUMERIC_RE = /^\d+$/;
|
|
3385
|
+
/** diff 新增项 → 事件载荷(operator = add_digest_uin,非数字串省略)。 */
|
|
3386
|
+
function essenceAddedData(groupCode, item) {
|
|
3387
|
+
const base = {
|
|
3388
|
+
group_id: Number(groupCode),
|
|
3389
|
+
message_seq: item.messageSeq
|
|
3390
|
+
};
|
|
3391
|
+
if (item.operatorUin !== null && NUMERIC_RE.test(item.operatorUin)) return {
|
|
3392
|
+
...base,
|
|
3393
|
+
operator_id: Number(item.operatorUin),
|
|
3394
|
+
is_set: true
|
|
3395
|
+
};
|
|
3396
|
+
return {
|
|
3397
|
+
...base,
|
|
3398
|
+
is_set: true
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
/** 变更列表 → changedList 条目数组(形状不符 → 空)。 */
|
|
3402
|
+
function changedListOf(list) {
|
|
3403
|
+
if (list === null || typeof list !== "object") return [];
|
|
3404
|
+
const changed = list.changedList;
|
|
3405
|
+
return Array.isArray(changed) ? changed : [];
|
|
3406
|
+
}
|
|
3407
|
+
/** 变更条目 → 翻转检测输入(null = 条目不支持/缺置顶字段)。 */
|
|
3408
|
+
function pinPayloadOf(entry) {
|
|
3409
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
3410
|
+
return pinPayloadFrom(entry);
|
|
3411
|
+
}
|
|
3412
|
+
function pinPayloadFrom(contact) {
|
|
3413
|
+
const topFlag = contact.topFlag;
|
|
3414
|
+
const payload = toPeerPinChangeData(contact);
|
|
3415
|
+
if (topFlag === void 0 || payload === null) return null;
|
|
3416
|
+
return {
|
|
3417
|
+
chatType: contact.chatType,
|
|
3418
|
+
peerUin: payload.peerUin,
|
|
3419
|
+
topFlag,
|
|
3420
|
+
topFlagTime: payload.topFlagTime,
|
|
3421
|
+
data: payload.data
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
/** 秒级时间戳归一(空/非法 → 当前时间;topFlagTime/msgTime 均为秒级字符串)。 */
|
|
3425
|
+
function secondsOf(raw) {
|
|
3426
|
+
if (raw !== void 0 && NUMERIC_RE.test(raw)) return Number(raw);
|
|
3427
|
+
return Math.floor(Date.now() / 1e3);
|
|
3428
|
+
}
|
|
2856
3429
|
//#endregion
|
|
2857
3430
|
//#region src/milky/config.ts
|
|
2858
3431
|
/**
|