@zhin.js/adapter-icqq 1.0.70 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +1134 -51
  2. package/README.md +116 -431
  3. package/adapters/icqq.js +31 -0
  4. package/adapters/icqq.ts +42 -0
  5. package/agent/PERMITS.md +24 -0
  6. package/agent/prompt-sections/platform.ts +17 -0
  7. package/commands/endpoint/add/[[id]].js +3 -0
  8. package/commands/endpoint/add/[[id]].ts +3 -0
  9. package/commands/endpoint/list.js +3 -0
  10. package/commands/endpoint/list.ts +3 -0
  11. package/commands/endpoint/remove/[id].js +3 -0
  12. package/commands/endpoint/remove/[id].ts +3 -0
  13. package/lib/client.d.ts +19 -0
  14. package/lib/client.js +3 -0
  15. package/lib/cq-message.d.ts +14 -0
  16. package/lib/cq-message.js +106 -0
  17. package/lib/endpoint.d.ts +31 -0
  18. package/lib/endpoint.js +1046 -0
  19. package/lib/forward-msg.d.ts +6 -0
  20. package/lib/forward-msg.js +107 -0
  21. package/lib/icqq-endpoint-commands.d.ts +1 -0
  22. package/lib/icqq-endpoint-commands.js +26 -0
  23. package/lib/icqq-guild.d.ts +57 -0
  24. package/lib/icqq-guild.js +109 -0
  25. package/lib/icqq-inbound.d.ts +165 -0
  26. package/lib/icqq-inbound.js +651 -0
  27. package/lib/icqq-inbox.d.ts +25 -0
  28. package/lib/icqq-inbox.js +129 -0
  29. package/lib/icqq-login-assist.d.ts +27 -0
  30. package/lib/icqq-login-assist.js +109 -0
  31. package/lib/icqq-runtime-state.d.ts +1 -0
  32. package/lib/icqq-runtime-state.js +6 -0
  33. package/lib/index.d.ts +5 -18
  34. package/lib/index.js +5 -124
  35. package/lib/outbound-media.d.ts +14 -0
  36. package/lib/outbound-media.js +76 -0
  37. package/lib/protocol.d.ts +276 -0
  38. package/lib/protocol.js +425 -0
  39. package/lib/types.d.ts +37 -11
  40. package/lib/types.js +0 -1
  41. package/package.json +80 -32
  42. package/plugin.js +19 -0
  43. package/schema.json +162 -0
  44. package/src/client.ts +29 -0
  45. package/src/cq-message.ts +113 -0
  46. package/src/endpoint.ts +1138 -0
  47. package/src/forward-msg.ts +112 -0
  48. package/src/icqq-endpoint-commands.ts +28 -0
  49. package/src/icqq-guild.ts +154 -0
  50. package/src/icqq-inbound.ts +814 -0
  51. package/src/icqq-inbox.ts +147 -0
  52. package/src/icqq-login-assist.ts +132 -0
  53. package/src/icqq-runtime-state.ts +7 -0
  54. package/src/index.ts +30 -149
  55. package/src/outbound-media.ts +103 -0
  56. package/src/protocol.ts +575 -0
  57. package/src/types.ts +40 -11
  58. package/tools/announce.js +16 -0
  59. package/tools/announce.ts +15 -0
  60. package/tools/essence.js +22 -0
  61. package/tools/essence.ts +20 -0
  62. package/tools/friend_list.js +18 -0
  63. package/tools/friend_list.ts +18 -0
  64. package/tools/group_files.js +22 -0
  65. package/tools/group_files.ts +20 -0
  66. package/tools/group_invite.js +23 -0
  67. package/tools/group_invite.ts +20 -0
  68. package/tools/group_kick.js +26 -0
  69. package/tools/group_kick.ts +22 -0
  70. package/tools/group_list.js +19 -0
  71. package/tools/group_list.ts +19 -0
  72. package/tools/group_member_info.js +24 -0
  73. package/tools/group_member_info.ts +26 -0
  74. package/tools/group_sign.js +19 -0
  75. package/tools/group_sign.ts +17 -0
  76. package/tools/list_muted.js +17 -0
  77. package/tools/list_muted.ts +16 -0
  78. package/tools/poke.js +16 -0
  79. package/tools/poke.ts +15 -0
  80. package/tools/send_user_like.js +25 -0
  81. package/tools/send_user_like.ts +22 -0
  82. package/tools/set_anonymous.js +21 -0
  83. package/tools/set_anonymous.ts +19 -0
  84. package/tools/set_mute.js +24 -0
  85. package/tools/set_mute.ts +21 -0
  86. package/tools/set_title.js +25 -0
  87. package/tools/set_title.ts +22 -0
  88. package/tools/user_info.js +14 -0
  89. package/tools/user_info.ts +13 -0
  90. package/LICENSE +0 -21
  91. package/client/ICQQManagement.tsx +0 -139
  92. package/client/LoginAssistPanel.tsx +0 -293
  93. package/client/index.tsx +0 -11
  94. package/client/tsconfig.json +0 -7
  95. package/client/utils/api.ts +0 -20
  96. package/dist/index.js +0 -1
  97. package/lib/adapter.d.ts +0 -34
  98. package/lib/adapter.d.ts.map +0 -1
  99. package/lib/adapter.js +0 -396
  100. package/lib/adapter.js.map +0 -1
  101. package/lib/bot.d.ts +0 -46
  102. package/lib/bot.d.ts.map +0 -1
  103. package/lib/bot.js +0 -457
  104. package/lib/bot.js.map +0 -1
  105. package/lib/index.d.ts.map +0 -1
  106. package/lib/index.js.map +0 -1
  107. package/lib/types.d.ts.map +0 -1
  108. package/lib/types.js.map +0 -1
  109. package/skills/icqq/SKILL.md +0 -22
  110. package/src/adapter.ts +0 -448
  111. package/src/bot.ts +0 -521
@@ -0,0 +1,651 @@
1
+ /**
2
+ * icqq 入站事件归一化(post_type / message_type / message 段)
3
+ */
4
+ import { toCanonicalSegments } from "zhin.js";
5
+ import { parseCqMessage, icqqMediaRefFromString } from "./cq-message.js";
6
+ import { extractForwardResidFromJsonElement } from "./forward-msg.js";
7
+ /**
8
+ * Resolve ICQQ's opaque protobuf media references while the endpoint-owned
9
+ * native client is still available. Resolution failure preserves the opaque
10
+ * reference, so canonical ingress stays truthful and can report unsupported
11
+ * media instead of inventing a URL.
12
+ */
13
+ export async function resolveIcqqInboundMedia(elements, resolver) {
14
+ if (!elements?.length)
15
+ return;
16
+ await Promise.all(elements.map(async (element) => {
17
+ if (!element || typeof element !== "object")
18
+ return;
19
+ if (typeof element.url === "string" && element.url)
20
+ return;
21
+ try {
22
+ const url = element.type === "record" || element.type === "audio"
23
+ ? await resolver.getPttUrl(element)
24
+ : element.type === "video"
25
+ ? await resolver.getVideoUrl(element)
26
+ : undefined;
27
+ if (typeof url === "string" && url)
28
+ element.url = url;
29
+ }
30
+ catch {
31
+ // The original platform reference remains the canonical fallback.
32
+ }
33
+ }));
34
+ }
35
+ const DEDUPE_TTL_MS = 120_000;
36
+ const ICQQ_GROUP_ROLES = new Set(["owner", "admin", "member"]);
37
+ /** 从 sender.role 解析群成员角色(供 Message.$sender → resolveSenderRoles) */
38
+ export function parseIcqqSenderGroupRole(sender) {
39
+ const raw = sender?.role;
40
+ if (typeof raw !== "string" || !raw.trim())
41
+ return undefined;
42
+ const role = raw.trim().toLowerCase();
43
+ return ICQQ_GROUP_ROLES.has(role) ? role : undefined;
44
+ }
45
+ /**
46
+ * 判断入站消息是否 @ 了本机账号(uin = endpoint 配置 name,即 QQ 号)。
47
+ * 新 Plugin Runtime 的 Message.content 为纯文本,at 信息只能经 metadata 传递,
48
+ * endpoint 在 admit 时用本函数标注 `metadata.mentioned`(供 Agent Host 的 @ 触发)。
49
+ */
50
+ export function isIcqqBotMentioned(input) {
51
+ const uin = String(input.uin ?? "").trim();
52
+ if (!uin)
53
+ return false;
54
+ for (const seg of input.content ?? []) {
55
+ if (!seg || (seg.type !== "at" && seg.type !== "mention"))
56
+ continue;
57
+ const data = (seg.data ?? {});
58
+ const target = String(data.qq ?? data.user_id ?? data.id ?? data.target ?? "").trim();
59
+ if (target && target !== "all" && Number(target) === Number(uin)) {
60
+ return true;
61
+ }
62
+ }
63
+ const raw = input.rawMessage ?? "";
64
+ if (!raw)
65
+ return false;
66
+ const escaped = uin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
67
+ // OneBot CQ 码 `[CQ:at,qq=uin]`;QQ 频道 markdown `<@!uin>`
68
+ return new RegExp(`\\[CQ:at,qq=${escaped}\\]`, "u").test(raw)
69
+ || new RegExp(`<@!?${escaped}>`, "u").test(raw);
70
+ }
71
+ const MESSAGE_POST_TYPES = new Set(["message"]);
72
+ function isMessagePostType(postType) {
73
+ if (typeof postType !== "string" || postType === "")
74
+ return false;
75
+ if (MESSAGE_POST_TYPES.has(postType))
76
+ return true;
77
+ // 如 message.private / message.group
78
+ return postType.startsWith("message.");
79
+ }
80
+ /** 从事件壳中取出 OneBot/icqq 载荷(兼容 data 嵌套或字段在根级) */
81
+ export function unwrapIcqqEventPayload(event) {
82
+ const nested = event.data;
83
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
84
+ const d = nested;
85
+ if (isRecognizedIcqqPayload(d)) {
86
+ return nested;
87
+ }
88
+ for (const key of ["data", "detail", "payload", "event"]) {
89
+ const inner = d[key];
90
+ if (inner && typeof inner === "object" && !Array.isArray(inner)) {
91
+ return inner;
92
+ }
93
+ }
94
+ }
95
+ const { id: _id, event: _ev, data: _data, ok: _ok, ...rest } = event;
96
+ if (isRecognizedIcqqPayload(rest)) {
97
+ return rest;
98
+ }
99
+ return nested;
100
+ }
101
+ function isRecognizedIcqqPayload(d) {
102
+ const pt = d.post_type;
103
+ if (typeof pt === "string") {
104
+ if (isMessagePostType(pt) ||
105
+ pt === "notice" ||
106
+ pt === "request" ||
107
+ pt === "meta_event" ||
108
+ pt.startsWith("notice.") ||
109
+ pt.startsWith("request.") ||
110
+ pt.startsWith("system.")) {
111
+ return true;
112
+ }
113
+ }
114
+ return (d.message_type != null ||
115
+ d.raw_message != null ||
116
+ d.message != null ||
117
+ d.type === "group" ||
118
+ d.type === "private" ||
119
+ d.notice_type != null ||
120
+ d.request_type != null ||
121
+ d.meta_event_type != null);
122
+ }
123
+ export function resolveIcqqInboundUserId(data) {
124
+ const sender = data.sender;
125
+ if (sender && typeof sender === "object" && !Array.isArray(sender)) {
126
+ const suid = sender.user_id;
127
+ if (suid != null)
128
+ return Number(suid);
129
+ }
130
+ if (data.user_id != null)
131
+ return Number(data.user_id);
132
+ if (data.from_id != null)
133
+ return Number(data.from_id);
134
+ return undefined;
135
+ }
136
+ export function isIcqqMessagePostType(data) {
137
+ if (!data || typeof data !== "object")
138
+ return false;
139
+ const d = data;
140
+ if (isMessagePostType(d.post_type))
141
+ return true;
142
+ // 其它 post_type(notice/request 等)明确排除
143
+ if (d.post_type != null && d.post_type !== "")
144
+ return false;
145
+ const hasBody = typeof d.raw_message === "string" ||
146
+ Array.isArray(d.message) ||
147
+ typeof d.message_type === "string" ||
148
+ d.type === "group" ||
149
+ d.type === "private";
150
+ return hasBody && resolveIcqqInboundUserId(d) != null;
151
+ }
152
+ export function shouldSkipSelfInboundMessage(data) {
153
+ if (data.self_id == null || data.user_id == null)
154
+ return false;
155
+ return Number(data.self_id) === Number(data.user_id);
156
+ }
157
+ /** 群临时会话私聊:icqq message_type=private & sub_type=group */
158
+ export function isIcqqGroupTempPrivateMessage(data) {
159
+ const messageType = data.message_type ??
160
+ (data.type === "group" || data.type === "private" ? data.type : undefined);
161
+ if (messageType !== "private")
162
+ return false;
163
+ if (data.sub_type === "group")
164
+ return true;
165
+ const groupId = data.sender?.group_id;
166
+ return typeof groupId === "number" && groupId > 0;
167
+ }
168
+ export function resolveChannelFromIcqqMessage(data) {
169
+ if (isIcqqGroupTempPrivateMessage(data)) {
170
+ const groupId = data.sender?.group_id ?? data.group_id;
171
+ const userId = resolveIcqqInboundUserId(data);
172
+ if (groupId != null && userId != null) {
173
+ return {
174
+ channelType: "private",
175
+ channelId: String(userId),
176
+ channelParentGroupId: String(groupId),
177
+ };
178
+ }
179
+ }
180
+ const messageType = data.message_type ??
181
+ (data.type === "group" || data.type === "private" ? data.type : undefined);
182
+ if (messageType === "group" || data.group_id != null) {
183
+ return {
184
+ channelType: "group",
185
+ channelId: String(data.group_id ?? data.from_id ?? data.user_id),
186
+ };
187
+ }
188
+ return {
189
+ channelType: "private",
190
+ channelId: String(data.from_id ?? data.user_id),
191
+ };
192
+ }
193
+ export function resolveIcqqInboundMessageId(data, channelId) {
194
+ if (data.message_id != null && String(data.message_id) !== "") {
195
+ return { id: String(data.message_id), source: "message_id" };
196
+ }
197
+ if (data.msg_id != null && String(data.msg_id) !== "") {
198
+ return { id: String(data.msg_id), source: "msg_id" };
199
+ }
200
+ if (data.seq != null && String(data.seq) !== "") {
201
+ return { id: String(data.seq), source: "seq" };
202
+ }
203
+ const uid = resolveIcqqInboundUserId(data) ?? 0;
204
+ return {
205
+ id: `${data.time}_${uid}_${channelId}`,
206
+ source: "synthetic",
207
+ };
208
+ }
209
+ export function icqqElementsToSegments(elements) {
210
+ if (!elements?.length)
211
+ return null;
212
+ const out = [];
213
+ for (const el of elements) {
214
+ if (!el || typeof el !== "object")
215
+ continue;
216
+ const type = el.type;
217
+ switch (type) {
218
+ case "text":
219
+ if (el.text != null && el.text !== "") {
220
+ out.push({ type: "text", data: { text: String(el.text) } });
221
+ }
222
+ break;
223
+ case "at": {
224
+ const atId = String(el.qq ?? el.user_id ?? "");
225
+ const name = typeof el.name === "string"
226
+ ? el.name
227
+ : typeof el.nickname === "string"
228
+ ? el.nickname
229
+ : undefined;
230
+ out.push({
231
+ type: "at",
232
+ data: name
233
+ ? { qq: atId, user_id: atId, id: atId, name }
234
+ : { qq: atId, user_id: atId, id: atId },
235
+ });
236
+ break;
237
+ }
238
+ case "face":
239
+ out.push({ type: "face", data: { id: Number(el.id) } });
240
+ break;
241
+ case "image": {
242
+ const value = String(el.url ?? el.file ?? "");
243
+ if (!value)
244
+ break;
245
+ out.push({
246
+ type: "image",
247
+ data: { media: icqqMediaRefFromElement(value, el) },
248
+ });
249
+ break;
250
+ }
251
+ case "flash": {
252
+ const value = String(el.url ?? el.file ?? "");
253
+ if (!value)
254
+ break;
255
+ out.push({
256
+ type: "image",
257
+ data: { media: icqqMediaRefFromElement(value, el), flash: true },
258
+ });
259
+ break;
260
+ }
261
+ case "forward": {
262
+ const forwardResid = String(el.id ?? el.resid ?? el.res_id ?? el.file ?? "").trim();
263
+ if (forwardResid) {
264
+ out.push({
265
+ type: "forward",
266
+ data: { id: forwardResid, resid: forwardResid },
267
+ });
268
+ }
269
+ else {
270
+ out.push({ type: "text", data: { text: "[聊天记录]" } });
271
+ }
272
+ break;
273
+ }
274
+ case "light_app":
275
+ case "xml": {
276
+ const forwardResid = extractForwardResidFromJsonElement(el);
277
+ if (forwardResid) {
278
+ out.push({
279
+ type: "forward",
280
+ data: { id: forwardResid, resid: forwardResid },
281
+ });
282
+ }
283
+ else if (el.text != null && el.text !== "") {
284
+ out.push({ type: "text", data: { text: String(el.text) } });
285
+ }
286
+ else {
287
+ out.push({ type: "text", data: { text: "[聊天记录]" } });
288
+ }
289
+ break;
290
+ }
291
+ case "reply": {
292
+ const replyId = String(el.id ?? el.message_id ?? "");
293
+ if (replyId) {
294
+ out.push({
295
+ type: "reply",
296
+ data: { id: replyId, message_id: replyId },
297
+ });
298
+ }
299
+ break;
300
+ }
301
+ case "json": {
302
+ const forwardResid = extractForwardResidFromJsonElement(el);
303
+ if (forwardResid) {
304
+ out.push({
305
+ type: "forward",
306
+ data: { id: forwardResid, resid: forwardResid },
307
+ });
308
+ break;
309
+ }
310
+ const replyFromJson = resolveReplyIdFromJsonElement(el);
311
+ if (replyFromJson) {
312
+ out.push({
313
+ type: "reply",
314
+ data: { id: replyFromJson, message_id: replyFromJson },
315
+ });
316
+ }
317
+ else if (el.text != null && el.text !== "") {
318
+ out.push({ type: "text", data: { text: String(el.text) } });
319
+ }
320
+ break;
321
+ }
322
+ case "record":
323
+ case "audio": {
324
+ const value = String(el.url ?? el.file ?? "");
325
+ if (!value)
326
+ break;
327
+ out.push({
328
+ type: "audio",
329
+ data: {
330
+ media: icqqMediaRefFromElement(value, el),
331
+ ...(typeof el.seconds === "number" ? { duration: el.seconds } : {}),
332
+ },
333
+ });
334
+ break;
335
+ }
336
+ case "video": {
337
+ const value = String(el.url ?? el.file ?? "");
338
+ if (!value)
339
+ break;
340
+ out.push({
341
+ type: "video",
342
+ data: {
343
+ media: icqqMediaRefFromElement(value, el),
344
+ ...(typeof el.seconds === "number" ? { duration: el.seconds } : {}),
345
+ },
346
+ });
347
+ break;
348
+ }
349
+ case "file": {
350
+ const value = String(el.url ?? el.file ?? el.fid ?? "");
351
+ if (!value)
352
+ break;
353
+ const name = typeof el.name === "string" && el.name ? el.name : undefined;
354
+ out.push({
355
+ type: "file",
356
+ data: {
357
+ media: icqqMediaRefFromElement(value, el),
358
+ ...(name ? { name } : {}),
359
+ },
360
+ });
361
+ break;
362
+ }
363
+ default:
364
+ if (el.text != null && el.text !== "") {
365
+ out.push({ type: "text", data: { text: String(el.text) } });
366
+ }
367
+ break;
368
+ }
369
+ }
370
+ return out.length ? out : null;
371
+ }
372
+ function icqqMediaRefFromElement(value, element) {
373
+ const media = icqqMediaRefFromString(value);
374
+ const fileName = typeof element.name === "string" && element.name
375
+ ? element.name
376
+ : undefined;
377
+ const size = typeof element.size === "number" && Number.isFinite(element.size)
378
+ ? element.size
379
+ : undefined;
380
+ return {
381
+ ...media,
382
+ ...(fileName ? { file_name: fileName } : {}),
383
+ ...(size != null ? { size } : {}),
384
+ };
385
+ }
386
+ function resolveReplyIdFromJsonElement(el) {
387
+ let payload = el.data ?? el;
388
+ if (typeof payload === "string") {
389
+ try {
390
+ payload = JSON.parse(payload);
391
+ }
392
+ catch {
393
+ return undefined;
394
+ }
395
+ }
396
+ if (!payload || typeof payload !== "object")
397
+ return undefined;
398
+ const record = payload;
399
+ const refer = record.msgRefer ??
400
+ record.reply ??
401
+ record.resid ??
402
+ record.message_id ??
403
+ record.id;
404
+ if (refer && typeof refer === "object") {
405
+ const r = refer;
406
+ const id = r.msgId ?? r.message_id ?? r.id ?? r.msg_id;
407
+ if (id != null && String(id))
408
+ return String(id);
409
+ }
410
+ if (typeof refer === "string" && refer)
411
+ return refer;
412
+ if (typeof refer === "number")
413
+ return String(refer);
414
+ return undefined;
415
+ }
416
+ const MAX_SOURCE_WALK_DEPTH = 10;
417
+ function icqqSourceMessageBody(s) {
418
+ return s.message;
419
+ }
420
+ function icqqSourceHasBody(s) {
421
+ const body = icqqSourceMessageBody(s);
422
+ if (Array.isArray(body) && body.length > 0)
423
+ return true;
424
+ if (typeof body === "string" && body.trim().length > 0)
425
+ return true;
426
+ return typeof s.raw_message === "string" && s.raw_message.length > 0;
427
+ }
428
+ function looksLikeIcqqMessageSource(value) {
429
+ if (!value || typeof value !== "object")
430
+ return false;
431
+ const r = value;
432
+ // 整条入站事件也有 message_id,不能与 oicq Message#source 混淆
433
+ if (r.post_type != null || r.message_type != null)
434
+ return false;
435
+ const s = value;
436
+ if (!icqqSourceHasBody(s))
437
+ return false;
438
+ return !!resolveQuoteIdFromIcqqSource(value);
439
+ }
440
+ /** 载荷可能把 source 嵌在 data / event 等字段,深度查找 */
441
+ export function findIcqqNestedMessageSource(root, maxDepth = MAX_SOURCE_WALK_DEPTH) {
442
+ const seen = new Set();
443
+ function walk(node, depth) {
444
+ if (depth > maxDepth || node == null)
445
+ return undefined;
446
+ if (typeof node !== "object")
447
+ return undefined;
448
+ if (seen.has(node))
449
+ return undefined;
450
+ seen.add(node);
451
+ if (looksLikeIcqqMessageSource(node)) {
452
+ return node;
453
+ }
454
+ const record = node;
455
+ if (record.source && looksLikeIcqqMessageSource(record.source)) {
456
+ return record.source;
457
+ }
458
+ for (const value of Object.values(record)) {
459
+ if (value && typeof value === "object") {
460
+ const hit = walk(value, depth + 1);
461
+ if (hit)
462
+ return hit;
463
+ }
464
+ }
465
+ return undefined;
466
+ }
467
+ return walk(root, 0);
468
+ }
469
+ /** 从 oicq MessageEvent.source 解析被引用消息的 message_id */
470
+ export function resolveQuoteIdFromIcqqSource(source) {
471
+ if (!source || typeof source !== "object")
472
+ return undefined;
473
+ const s = source;
474
+ if (s.message_id != null && String(s.message_id).trim()) {
475
+ return String(s.message_id).trim();
476
+ }
477
+ if (s.msg_id != null && String(s.msg_id).trim()) {
478
+ return String(s.msg_id).trim();
479
+ }
480
+ // NT 引用源常仅有 seq/rand,无 message_id
481
+ if (s.seq != null && String(s.seq) !== "") {
482
+ const seq = String(s.seq);
483
+ if (s.rand != null && String(s.rand) !== "") {
484
+ return `${seq}:${s.rand}`;
485
+ }
486
+ return seq;
487
+ }
488
+ return undefined;
489
+ }
490
+ function icqqSourceToContentSegments(s) {
491
+ const body = icqqSourceMessageBody(s);
492
+ if (Array.isArray(body) && body.length) {
493
+ return icqqElementsToSegments(body) ?? [];
494
+ }
495
+ if (typeof body === "string" && body.trim()) {
496
+ return [{ type: "text", data: { text: body.trim() } }];
497
+ }
498
+ if (typeof s.raw_message === "string" && s.raw_message) {
499
+ return parseCqMessage(s.raw_message);
500
+ }
501
+ return [];
502
+ }
503
+ /** Convert an ICQQ source into the adapter-local lookup payload. */
504
+ export function messageLookupFromIcqqSource(source) {
505
+ if (!source || typeof source !== "object")
506
+ return null;
507
+ const s = source;
508
+ const body = icqqSourceMessageBody(s);
509
+ const content = icqqSourceToContentSegments(s);
510
+ const messageId = resolveQuoteIdFromIcqqSource(source);
511
+ if (!messageId && !content.length)
512
+ return null;
513
+ const senderRaw = s.sender;
514
+ const senderFromUserId = s.user_id != null
515
+ ? { id: String(s.user_id), name: "" }
516
+ : undefined;
517
+ return {
518
+ messageId: messageId ?? `seq:${s.seq ?? 0}:${s.rand ?? 0}`,
519
+ sender: senderRaw
520
+ ? {
521
+ id: String(senderRaw.user_id ?? ""),
522
+ name: String(senderRaw.nickname ?? senderRaw.card ?? ""),
523
+ }
524
+ : senderFromUserId,
525
+ content,
526
+ raw: typeof s.raw_message === "string"
527
+ ? s.raw_message
528
+ : typeof body === "string"
529
+ ? body
530
+ : undefined,
531
+ time: typeof s.time === "number" ? s.time : undefined,
532
+ };
533
+ }
534
+ /** 从事件顶层字段解析引用 message_id(与 message 段互补) */
535
+ export function resolveIcqqQuoteIdFromEvent(data) {
536
+ const fromSource = resolveQuoteIdFromIcqqSource(data.source) ??
537
+ resolveQuoteIdFromIcqqSource(findIcqqNestedMessageSource(data));
538
+ if (fromSource)
539
+ return fromSource;
540
+ const ext = data;
541
+ const candidates = [
542
+ ext.reply,
543
+ ext.quoted_message_id,
544
+ ext.quote_message_id,
545
+ ext.source_message_id,
546
+ ];
547
+ for (const c of candidates) {
548
+ if (typeof c === "string" && c.trim())
549
+ return c.trim();
550
+ if (typeof c === "number")
551
+ return String(c);
552
+ if (c && typeof c === "object") {
553
+ const r = c;
554
+ const id = r.id ?? r.message_id ?? r.msg_id ?? r.msgId;
555
+ if (id != null && String(id))
556
+ return String(id);
557
+ }
558
+ }
559
+ return undefined;
560
+ }
561
+ function mergeReplyFromRawMessage(content, rawMessage) {
562
+ if (!rawMessage || quoteIdFromSegments(content))
563
+ return content;
564
+ const fromRaw = parseCqMessage(rawMessage);
565
+ const quoteFromRaw = quoteIdFromSegments(fromRaw);
566
+ if (!quoteFromRaw)
567
+ return content;
568
+ return [
569
+ { type: "reply", data: { message_id: quoteFromRaw } },
570
+ ...content.filter((s) => s.type !== "reply"),
571
+ ];
572
+ }
573
+ function mergeReplyFromSource(content, data) {
574
+ if (quoteIdFromSegments(content))
575
+ return content;
576
+ const quoteId = resolveQuoteIdFromIcqqSource(data.source) ??
577
+ resolveQuoteIdFromIcqqSource(findIcqqNestedMessageSource(data));
578
+ if (!quoteId)
579
+ return content;
580
+ return [
581
+ { type: "reply", data: { message_id: quoteId } },
582
+ ...content.filter((s) => s.type !== "reply"),
583
+ ];
584
+ }
585
+ function quoteIdFromSegments(content) {
586
+ const reply = content.find((item) => item.type === 'reply');
587
+ const value = reply?.data?.message_id ?? reply?.data?.id;
588
+ return value == null || !String(value).trim() ? undefined : String(value).trim();
589
+ }
590
+ export function resolveInboundContent(data) {
591
+ const fromElements = icqqElementsToSegments(data.message);
592
+ const raw = data.raw_message ?? "";
593
+ let content;
594
+ if (fromElements?.length) {
595
+ content = mergeReplyFromRawMessage(fromElements, raw);
596
+ }
597
+ else if (raw) {
598
+ content = parseCqMessage(raw);
599
+ }
600
+ else {
601
+ content = [{ type: "text", data: { text: "" } }];
602
+ }
603
+ return toCanonicalSegments(mergeReplyFromSource(content, data));
604
+ }
605
+ export function normalizeIcqqInboundMessage(data) {
606
+ if (!isIcqqMessagePostType(data))
607
+ return null;
608
+ const ext = data;
609
+ const userIdNum = resolveIcqqInboundUserId(ext);
610
+ if (userIdNum == null)
611
+ return null;
612
+ const { channelType, channelId, channelParentGroupId } = resolveChannelFromIcqqMessage(data);
613
+ const resolved = resolveIcqqInboundMessageId(data, channelId);
614
+ const nickname = data.sender?.nickname ?? data.nickname ?? String(userIdNum);
615
+ const senderRole = channelType === "group"
616
+ ? parseIcqqSenderGroupRole(data.sender)
617
+ : undefined;
618
+ const rawMessage = data.raw_message ?? "";
619
+ return {
620
+ messageId: resolved.id,
621
+ idSource: resolved.source,
622
+ channelType,
623
+ channelId,
624
+ channelParentGroupId,
625
+ userId: String(userIdNum),
626
+ nickname,
627
+ senderRole,
628
+ content: resolveInboundContent(data),
629
+ rawMessage,
630
+ timestampMs: data.time * 1000,
631
+ raw: data,
632
+ };
633
+ }
634
+ /** 按 message_id 去重(多路 subscribe 会收到同一消息多次) */
635
+ export class InboundMessageDeduper {
636
+ seen = new Map();
637
+ shouldProcess(messageId) {
638
+ const now = Date.now();
639
+ for (const [id, t] of this.seen) {
640
+ if (now - t > DEDUPE_TTL_MS)
641
+ this.seen.delete(id);
642
+ }
643
+ if (this.seen.has(messageId))
644
+ return false;
645
+ this.seen.set(messageId, now);
646
+ return true;
647
+ }
648
+ clear() {
649
+ this.seen.clear();
650
+ }
651
+ }
@@ -0,0 +1,25 @@
1
+ import type { SystemMessage } from './types.js';
2
+ /** 行级公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
3
+ export interface IcqqInboxBase {
4
+ readonly adapter: string;
5
+ readonly endpointKey: string;
6
+ }
7
+ /** post_type=request / request.* 判定(OneBot 事件壳)。 */
8
+ export declare function isIcqqRequestPayload(payload: unknown): boolean;
9
+ /** post_type=notice / notice.* 判定。 */
10
+ export declare function isIcqqNoticePayload(payload: unknown): boolean;
11
+ /**
12
+ * request 事件载荷 → unified_inbox_request 行。
13
+ * platform_request_id 取 flag(审批回执句柄),缺省回退 seq;两者皆无则无法审批,丢弃。
14
+ */
15
+ export declare function buildIcqqInboxRequestRow(payload: Record<string, unknown>, base: IcqqInboxBase): Record<string, unknown> | null;
16
+ /**
17
+ * notice 事件载荷 → unified_inbox_notice 行。
18
+ * 事件本身无稳定 id,platform_notice_id 由 time + actor + type 合成(配合去重)。
19
+ */
20
+ export declare function buildIcqqInboxNoticeRow(payload: Record<string, unknown>, base: IcqqInboxBase): Record<string, unknown> | null;
21
+ /**
22
+ * GET_SYSTEM_MSG 返回项 → unified_inbox_request 行(启动时首次拉取)。
23
+ * friend 列表项 type 语义即好友请求;group 列表项 type 为 add/invite(记 sub_type)。
24
+ */
25
+ export declare function buildIcqqSystemRequestRow(message: SystemMessage, kind: 'friend' | 'group', base: IcqqInboxBase): Record<string, unknown> | null;