@zhin.js/adapter-icqq 8.0.12 → 8.0.14
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/CHANGELOG.md +55 -0
- package/README.md +8 -4
- package/adapters/icqq.js +3 -55
- package/adapters/icqq.ts +12 -62
- package/lib/cq-message.d.ts +5 -13
- package/lib/cq-message.js +6 -72
- package/lib/endpoint.d.ts +9 -10
- package/lib/endpoint.js +429 -141
- package/lib/forward-msg.d.ts +3 -22
- package/lib/forward-msg.js +38 -307
- package/lib/icqq-inbound.d.ts +29 -3
- package/lib/icqq-inbound.js +95 -12
- package/lib/icqq-login-assist.d.ts +27 -0
- package/lib/icqq-login-assist.js +109 -0
- package/lib/index.d.ts +1 -1
- package/lib/outbound-media.d.ts +6 -2
- package/lib/outbound-media.js +15 -8
- package/lib/protocol.d.ts +10 -1
- package/lib/protocol.js +82 -20
- package/package.json +13 -13
- package/src/cq-message.ts +7 -74
- package/src/endpoint.ts +463 -153
- package/src/forward-msg.ts +35 -342
- package/src/icqq-inbound.ts +112 -13
- package/src/icqq-login-assist.ts +132 -0
- package/src/index.ts +0 -1
- package/src/outbound-media.ts +20 -5
- package/src/protocol.ts +82 -22
- package/tools/announce.js +1 -1
- package/tools/announce.ts +1 -1
- package/tools/essence.js +1 -1
- package/tools/essence.ts +1 -1
- package/tools/friend_list.js +5 -4
- package/tools/friend_list.ts +7 -5
- package/tools/group_list.js +6 -5
- package/tools/group_list.ts +8 -6
- package/tools/group_member_info.js +27 -0
- package/tools/group_member_info.ts +30 -0
- package/tools/set_mute.js +1 -1
- package/tools/set_mute.ts +1 -1
- package/tools/set_title.js +1 -1
- package/tools/set_title.ts +1 -1
package/lib/forward-msg.d.ts
CHANGED
|
@@ -1,25 +1,6 @@
|
|
|
1
|
-
import { type MessageSegment, type QuotedMessagePayload } from 'zhin.js';
|
|
2
1
|
import { type IcqqMessageElement } from "./icqq-inbound.js";
|
|
2
|
+
import type { ForwardEntry } from '@zhin.js/im-contract';
|
|
3
3
|
/** 从 QQ 合并转发 json 元素提取 resid */
|
|
4
4
|
export declare function extractForwardResidFromJsonElement(el: IcqqMessageElement): string | undefined;
|
|
5
|
-
/**
|
|
6
|
-
export declare function
|
|
7
|
-
/** 从 get_msg 的 message 元素数组解析 multimsg resid */
|
|
8
|
-
export declare function extractForwardResidFromGetMsgElements(elements: IcqqMessageElement[] | undefined): string | undefined;
|
|
9
|
-
/**
|
|
10
|
-
* 从 get_msg 响应解析合并转发 resid(优先 message 数组,其次 raw_message)。
|
|
11
|
-
*/
|
|
12
|
-
export declare function extractForwardResidFromGetMsg(data: unknown): string | undefined;
|
|
13
|
-
export declare function extractForwardResidFromSegments(content: MessageSegment[]): string | undefined;
|
|
14
|
-
export declare function extractForwardResidFromPayload(payload: QuotedMessagePayload): string | undefined;
|
|
15
|
-
/** 在 get_msg 原始对象里深度搜索 resid */
|
|
16
|
-
export declare function extractForwardResidDeep(root: unknown, maxDepth?: number): string | undefined;
|
|
17
|
-
export declare function isForwardPlaceholderPayload(payload: QuotedMessagePayload): boolean;
|
|
18
|
-
/** 将 get_forward_msg 响应格式化为可读聊天记录 */
|
|
19
|
-
export declare function formatForwardMsgResponse(data: unknown): string;
|
|
20
|
-
export declare function fetchForwardMsgText(client: {
|
|
21
|
-
getForwardMsg(resId: string, fileName?: string): Promise<unknown[]>;
|
|
22
|
-
}, id: string): Promise<string>;
|
|
23
|
-
export declare function enrichQuotedPayloadWithForward(client: {
|
|
24
|
-
getForwardMsg(resId: string, fileName?: string): Promise<unknown[]>;
|
|
25
|
-
} | null | undefined, payload: QuotedMessagePayload, rawData?: unknown): Promise<QuotedMessagePayload>;
|
|
5
|
+
/** Canonical merged-forward entries. Speakers are data actors, never LLM roles. */
|
|
6
|
+
export declare function normalizeForwardMsgResponse(data: unknown): readonly ForwardEntry[];
|
package/lib/forward-msg.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { segment } from 'zhin.js';
|
|
2
1
|
import { icqqElementsToSegments, } from "./icqq-inbound.js";
|
|
3
|
-
import {
|
|
2
|
+
import { toCanonicalSegments } from '@zhin.js/core';
|
|
4
3
|
const RESID_IN_XML_RE = /(?:m_resid|resid|fileid|res_id)=["']?([A-Za-z0-9+/=_.-]{8,})/i;
|
|
5
|
-
const FORWARD_PLACEHOLDER_RE = /\[聊天记录\]|合并转发|转发消息|聊天记录/i;
|
|
6
4
|
function parseJsonPayload(raw) {
|
|
7
5
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
8
6
|
return raw;
|
|
@@ -64,313 +62,46 @@ export function extractForwardResidFromJsonElement(el) {
|
|
|
64
62
|
return String(direct).trim();
|
|
65
63
|
return undefined;
|
|
66
64
|
}
|
|
67
|
-
/**
|
|
68
|
-
export function
|
|
69
|
-
const
|
|
70
|
-
if (!text)
|
|
71
|
-
return undefined;
|
|
72
|
-
const xml = text.match(RESID_IN_XML_RE);
|
|
73
|
-
if (xml?.[1])
|
|
74
|
-
return xml[1].trim();
|
|
75
|
-
const asJson = parseJsonPayload(text);
|
|
76
|
-
if (asJson) {
|
|
77
|
-
const fromJson = extractForwardResidFromJsonElement({
|
|
78
|
-
type: "json",
|
|
79
|
-
data: asJson,
|
|
80
|
-
});
|
|
81
|
-
if (fromJson)
|
|
82
|
-
return fromJson;
|
|
83
|
-
}
|
|
84
|
-
for (const match of text.matchAll(/\[CQ:json(?:,([^\]]*))?\]/gi)) {
|
|
85
|
-
const arg = match[1] ?? "";
|
|
86
|
-
const dataMatch = arg.match(/(?:^|,)data=(.+)$/s);
|
|
87
|
-
if (!dataMatch?.[1])
|
|
88
|
-
continue;
|
|
89
|
-
const record = parseJsonPayload(dataMatch[1].trim());
|
|
90
|
-
if (!record)
|
|
91
|
-
continue;
|
|
92
|
-
const fromCq = extractForwardResidFromJsonElement({
|
|
93
|
-
type: "json",
|
|
94
|
-
data: record,
|
|
95
|
-
});
|
|
96
|
-
if (fromCq)
|
|
97
|
-
return fromCq;
|
|
98
|
-
}
|
|
99
|
-
const segs = parseCqMessage(text);
|
|
100
|
-
return extractForwardResidFromSegments(segs);
|
|
101
|
-
}
|
|
102
|
-
/** 从 get_msg 的 message 元素数组解析 multimsg resid */
|
|
103
|
-
export function extractForwardResidFromGetMsgElements(elements) {
|
|
104
|
-
if (!elements?.length)
|
|
105
|
-
return undefined;
|
|
106
|
-
for (const el of elements) {
|
|
107
|
-
if (!el || typeof el !== "object")
|
|
108
|
-
continue;
|
|
109
|
-
if (el.type === "forward") {
|
|
110
|
-
const id = String(el.id ?? el.resid ?? el.res_id ?? el.file ?? "").trim();
|
|
111
|
-
if (id)
|
|
112
|
-
return id;
|
|
113
|
-
}
|
|
114
|
-
const fromJson = extractForwardResidFromJsonElement(el);
|
|
115
|
-
if (fromJson)
|
|
116
|
-
return fromJson;
|
|
117
|
-
}
|
|
118
|
-
const segs = icqqElementsToSegments(elements);
|
|
119
|
-
if (segs?.length) {
|
|
120
|
-
return extractForwardResidFromSegments(segs);
|
|
121
|
-
}
|
|
122
|
-
return extractForwardResidDeep(elements);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* 从 get_msg 响应解析合并转发 resid(优先 message 数组,其次 raw_message)。
|
|
126
|
-
*/
|
|
127
|
-
export function extractForwardResidFromGetMsg(data) {
|
|
128
|
-
if (!data || typeof data !== "object")
|
|
129
|
-
return undefined;
|
|
130
|
-
const record = data;
|
|
131
|
-
if (Array.isArray(record.message)) {
|
|
132
|
-
const fromElements = extractForwardResidFromGetMsgElements(record.message);
|
|
133
|
-
if (fromElements)
|
|
134
|
-
return fromElements;
|
|
135
|
-
}
|
|
136
|
-
if (typeof record.raw_message === "string" && record.raw_message.trim()) {
|
|
137
|
-
const fromRaw = extractForwardResidFromRawMessage(record.raw_message);
|
|
138
|
-
if (fromRaw)
|
|
139
|
-
return fromRaw;
|
|
140
|
-
}
|
|
141
|
-
if (typeof record.message === "string" && record.message.trim()) {
|
|
142
|
-
return extractForwardResidFromRawMessage(record.message);
|
|
143
|
-
}
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
export function extractForwardResidFromSegments(content) {
|
|
147
|
-
for (const seg of content) {
|
|
148
|
-
if (seg.type === "forward") {
|
|
149
|
-
const id = seg.data?.id ?? seg.data?.resid;
|
|
150
|
-
if (id != null && String(id).trim())
|
|
151
|
-
return String(id).trim();
|
|
152
|
-
}
|
|
153
|
-
if (seg.type === "json" && seg.data) {
|
|
154
|
-
const resid = extractForwardResidFromJsonElement(seg.data);
|
|
155
|
-
if (resid)
|
|
156
|
-
return resid;
|
|
157
|
-
const raw = seg.data.data ?? seg.data.text;
|
|
158
|
-
const record = parseJsonPayload(raw);
|
|
159
|
-
if (record) {
|
|
160
|
-
const fromEl = extractForwardResidFromJsonElement({
|
|
161
|
-
type: "json",
|
|
162
|
-
data: record,
|
|
163
|
-
});
|
|
164
|
-
if (fromEl)
|
|
165
|
-
return fromEl;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
const raw = segment.raw(content);
|
|
170
|
-
const m = raw.match(RESID_IN_XML_RE);
|
|
171
|
-
return m?.[1]?.trim();
|
|
172
|
-
}
|
|
173
|
-
export function extractForwardResidFromPayload(payload) {
|
|
174
|
-
if (Array.isArray(payload.content)) {
|
|
175
|
-
const fromSeg = extractForwardResidFromSegments(payload.content);
|
|
176
|
-
if (fromSeg)
|
|
177
|
-
return fromSeg;
|
|
178
|
-
}
|
|
179
|
-
if (typeof payload.raw === "string") {
|
|
180
|
-
const m = payload.raw.match(RESID_IN_XML_RE);
|
|
181
|
-
if (m?.[1])
|
|
182
|
-
return m[1].trim();
|
|
183
|
-
const record = parseJsonPayload(payload.raw);
|
|
184
|
-
if (record) {
|
|
185
|
-
return extractForwardResidFromJsonElement({ type: "json", data: record });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return undefined;
|
|
189
|
-
}
|
|
190
|
-
/** 在 get_msg 原始对象里深度搜索 resid */
|
|
191
|
-
export function extractForwardResidDeep(root, maxDepth = 14) {
|
|
192
|
-
const seen = new Set();
|
|
193
|
-
function walk(node, depth) {
|
|
194
|
-
if (depth > maxDepth || node == null)
|
|
195
|
-
return undefined;
|
|
196
|
-
if (typeof node === "string") {
|
|
197
|
-
const m = node.match(RESID_IN_XML_RE);
|
|
198
|
-
if (m?.[1])
|
|
199
|
-
return m[1].trim();
|
|
200
|
-
const record = parseJsonPayload(node);
|
|
201
|
-
if (record) {
|
|
202
|
-
const fromJson = extractForwardResidFromJsonElement({
|
|
203
|
-
type: "json",
|
|
204
|
-
data: record,
|
|
205
|
-
});
|
|
206
|
-
if (fromJson)
|
|
207
|
-
return fromJson;
|
|
208
|
-
}
|
|
209
|
-
return undefined;
|
|
210
|
-
}
|
|
211
|
-
if (typeof node !== "object")
|
|
212
|
-
return undefined;
|
|
213
|
-
if (seen.has(node))
|
|
214
|
-
return undefined;
|
|
215
|
-
seen.add(node);
|
|
216
|
-
if (Array.isArray(node)) {
|
|
217
|
-
for (const item of node) {
|
|
218
|
-
const hit = walk(item, depth + 1);
|
|
219
|
-
if (hit)
|
|
220
|
-
return hit;
|
|
221
|
-
}
|
|
222
|
-
return undefined;
|
|
223
|
-
}
|
|
224
|
-
const fromEl = extractForwardResidFromJsonElement({
|
|
225
|
-
type: "json",
|
|
226
|
-
data: node,
|
|
227
|
-
});
|
|
228
|
-
if (fromEl)
|
|
229
|
-
return fromEl;
|
|
230
|
-
for (const value of Object.values(node)) {
|
|
231
|
-
const hit = walk(value, depth + 1);
|
|
232
|
-
if (hit)
|
|
233
|
-
return hit;
|
|
234
|
-
}
|
|
235
|
-
return undefined;
|
|
236
|
-
}
|
|
237
|
-
return walk(root, 0);
|
|
238
|
-
}
|
|
239
|
-
export function isForwardPlaceholderPayload(payload) {
|
|
240
|
-
if (Array.isArray(payload.content) &&
|
|
241
|
-
payload.content.some((s) => s.type === "forward")) {
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
const raw = Array.isArray(payload.content)
|
|
245
|
-
? segment.raw(payload.content)
|
|
246
|
-
: String(payload.content ?? payload.raw ?? "");
|
|
247
|
-
return FORWARD_PLACEHOLDER_RE.test(raw);
|
|
248
|
-
}
|
|
249
|
-
function hasMergedForwardBlock(payload) {
|
|
250
|
-
const raw = Array.isArray(payload.content)
|
|
251
|
-
? segment.raw(payload.content)
|
|
252
|
-
: String(payload.content ?? "");
|
|
253
|
-
return raw.includes("[Merged chat history");
|
|
254
|
-
}
|
|
255
|
-
function senderLabel(sender) {
|
|
256
|
-
if (!sender || typeof sender !== "object")
|
|
257
|
-
return "unknown";
|
|
258
|
-
const s = sender;
|
|
259
|
-
const name = s.nickname ?? s.name ?? s.card;
|
|
260
|
-
const id = s.user_id ?? s.uin ?? s.uid ?? s.id;
|
|
261
|
-
if (name && id)
|
|
262
|
-
return `${name} (${id})`;
|
|
263
|
-
if (name)
|
|
264
|
-
return String(name);
|
|
265
|
-
if (id != null)
|
|
266
|
-
return String(id);
|
|
267
|
-
return "unknown";
|
|
268
|
-
}
|
|
269
|
-
function messageBodyToText(body) {
|
|
270
|
-
if (body == null)
|
|
271
|
-
return "";
|
|
272
|
-
if (typeof body === "string")
|
|
273
|
-
return body.trim();
|
|
274
|
-
if (Array.isArray(body)) {
|
|
275
|
-
const segs = icqqElementsToSegments(body);
|
|
276
|
-
return segs?.length ? segment.raw(segs).trim() : segment.raw(body).trim();
|
|
277
|
-
}
|
|
278
|
-
if (typeof body === "object") {
|
|
279
|
-
const r = body;
|
|
280
|
-
if (typeof r.raw_message === "string" && r.raw_message.trim()) {
|
|
281
|
-
return r.raw_message.trim();
|
|
282
|
-
}
|
|
283
|
-
if (Array.isArray(r.message)) {
|
|
284
|
-
return messageBodyToText(r.message);
|
|
285
|
-
}
|
|
286
|
-
if (Array.isArray(r.elements)) {
|
|
287
|
-
return messageBodyToText(r.elements);
|
|
288
|
-
}
|
|
289
|
-
if (typeof r.content === "string")
|
|
290
|
-
return r.content.trim();
|
|
291
|
-
}
|
|
292
|
-
return "";
|
|
293
|
-
}
|
|
294
|
-
/** 将 get_forward_msg 响应格式化为可读聊天记录 */
|
|
295
|
-
export function formatForwardMsgResponse(data) {
|
|
296
|
-
if (!data)
|
|
297
|
-
return "";
|
|
298
|
-
const root = data && typeof data === "object" && !Array.isArray(data)
|
|
65
|
+
/** Canonical merged-forward entries. Speakers are data actors, never LLM roles. */
|
|
66
|
+
export function normalizeForwardMsgResponse(data) {
|
|
67
|
+
const root = data && typeof data === 'object' && !Array.isArray(data)
|
|
299
68
|
? data
|
|
300
|
-
:
|
|
69
|
+
: undefined;
|
|
301
70
|
const list = Array.isArray(data)
|
|
302
71
|
? data
|
|
303
|
-
: Array.isArray(root?.messages)
|
|
304
|
-
? root.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
: Array.isArray(root?.msg_list)
|
|
308
|
-
? root.msg_list
|
|
309
|
-
: Array.isArray(root?.message)
|
|
310
|
-
? root.message
|
|
72
|
+
: Array.isArray(root?.messages) ? root.messages
|
|
73
|
+
: Array.isArray(root?.msgList) ? root.msgList
|
|
74
|
+
: Array.isArray(root?.msg_list) ? root.msg_list
|
|
75
|
+
: Array.isArray(root?.message) ? root.message
|
|
311
76
|
: [];
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
let index = 0;
|
|
316
|
-
for (const item of list) {
|
|
317
|
-
if (!item || typeof item !== "object")
|
|
318
|
-
continue;
|
|
77
|
+
return Object.freeze(list.flatMap((item) => {
|
|
78
|
+
if (!item || typeof item !== 'object')
|
|
79
|
+
return [];
|
|
319
80
|
const row = item;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (Array.isArray(payload.content)) {
|
|
347
|
-
return {
|
|
348
|
-
...payload,
|
|
349
|
-
content: [...payload.content, { type: "text", data: { text: block } }],
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
const prev = typeof payload.content === "string" ? payload.content.trim() : "";
|
|
353
|
-
return {
|
|
354
|
-
...payload,
|
|
355
|
-
content: prev ? `${prev}\n\n${block}` : block,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
export async function enrichQuotedPayloadWithForward(client, payload, rawData) {
|
|
359
|
-
if (!client || hasMergedForwardBlock(payload))
|
|
360
|
-
return payload;
|
|
361
|
-
const resid = extractForwardResidFromPayload(payload) ??
|
|
362
|
-
(rawData ? extractForwardResidFromGetMsg(rawData) : undefined) ??
|
|
363
|
-
(rawData ? extractForwardResidDeep(rawData) : undefined);
|
|
364
|
-
const fetchIds = [
|
|
365
|
-
resid,
|
|
366
|
-
isForwardPlaceholderPayload(payload) ? payload.messageId : undefined,
|
|
367
|
-
].filter((v, i, a) => !!v && a.indexOf(v) === i);
|
|
368
|
-
for (const fetchId of fetchIds) {
|
|
369
|
-
const forwardText = await fetchForwardMsgText(client, fetchId);
|
|
370
|
-
if (!forwardText.trim())
|
|
371
|
-
continue;
|
|
372
|
-
const block = `[Merged chat history — id ${fetchId}]\n${forwardText.trim()}`;
|
|
373
|
-
return appendForwardBlock(payload, block);
|
|
374
|
-
}
|
|
375
|
-
return payload;
|
|
81
|
+
const sender = row.sender && typeof row.sender === 'object'
|
|
82
|
+
? row.sender
|
|
83
|
+
: row.user && typeof row.user === 'object'
|
|
84
|
+
? row.user
|
|
85
|
+
: undefined;
|
|
86
|
+
const id = sender?.user_id ?? sender?.uin ?? sender?.id;
|
|
87
|
+
const displayName = sender?.nickname ?? sender?.card ?? sender?.name;
|
|
88
|
+
const body = row.message ?? row.content ?? row.elements ?? row.raw_message;
|
|
89
|
+
let segments;
|
|
90
|
+
if (Array.isArray(body))
|
|
91
|
+
segments = toCanonicalSegments(icqqElementsToSegments(body) ?? body);
|
|
92
|
+
else if (typeof body === 'string' && body.trim())
|
|
93
|
+
segments = [{ type: 'text', data: { text: body.trim() } }];
|
|
94
|
+
else
|
|
95
|
+
segments = [];
|
|
96
|
+
if (segments.length === 0)
|
|
97
|
+
return [];
|
|
98
|
+
return [Object.freeze({
|
|
99
|
+
...(id != null ? { actor: Object.freeze({
|
|
100
|
+
id: String(id),
|
|
101
|
+
...(displayName != null && String(displayName).trim() ? { displayName: String(displayName) } : {}),
|
|
102
|
+
}) } : {}),
|
|
103
|
+
...(typeof row.time === 'number' ? { timestamp: row.time < 1e12 ? row.time * 1000 : row.time } : {}),
|
|
104
|
+
segments: Object.freeze(segments),
|
|
105
|
+
})];
|
|
106
|
+
}));
|
|
376
107
|
}
|
package/lib/icqq-inbound.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* icqq 入站事件归一化(post_type / message_type / message 段)
|
|
3
3
|
*/
|
|
4
|
-
import { type MessageSegment
|
|
4
|
+
import { type MessageSegment } from "zhin.js";
|
|
5
5
|
import type { GroupRole } from "./types.js";
|
|
6
6
|
/** 守护进程推送的通用事件壳 */
|
|
7
7
|
export interface IcqqEventBase {
|
|
@@ -60,6 +60,16 @@ export interface IcqqMessageSource {
|
|
|
60
60
|
};
|
|
61
61
|
[key: string]: unknown;
|
|
62
62
|
}
|
|
63
|
+
interface IcqqMessageLookup {
|
|
64
|
+
readonly messageId: string;
|
|
65
|
+
readonly sender?: Readonly<{
|
|
66
|
+
id?: string;
|
|
67
|
+
name?: string;
|
|
68
|
+
}>;
|
|
69
|
+
readonly content: readonly MessageSegment[] | string;
|
|
70
|
+
readonly raw?: string;
|
|
71
|
+
readonly time?: number;
|
|
72
|
+
}
|
|
63
73
|
export type IcqqMessageElement = {
|
|
64
74
|
type: string;
|
|
65
75
|
text?: string;
|
|
@@ -68,8 +78,23 @@ export type IcqqMessageElement = {
|
|
|
68
78
|
id?: string | number;
|
|
69
79
|
url?: string;
|
|
70
80
|
file?: string;
|
|
81
|
+
fid?: string;
|
|
82
|
+
name?: string;
|
|
83
|
+
size?: number;
|
|
84
|
+
seconds?: number;
|
|
71
85
|
[key: string]: unknown;
|
|
72
86
|
};
|
|
87
|
+
export interface IcqqInboundMediaResolver {
|
|
88
|
+
getPttUrl(element: IcqqMessageElement): Promise<string | null | undefined>;
|
|
89
|
+
getVideoUrl(element: IcqqMessageElement): Promise<string | null | undefined>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve ICQQ's opaque protobuf media references while the endpoint-owned
|
|
93
|
+
* native client is still available. Resolution failure preserves the opaque
|
|
94
|
+
* reference, so canonical ingress stays truthful and can report unsupported
|
|
95
|
+
* media instead of inventing a URL.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveIcqqInboundMedia(elements: IcqqMessageElement[] | undefined, resolver: IcqqInboundMediaResolver): Promise<void>;
|
|
73
98
|
/** 归一化后供适配器使用的入站消息 */
|
|
74
99
|
export interface NormalizedIcqqInbound {
|
|
75
100
|
messageId: string;
|
|
@@ -125,8 +150,8 @@ export declare function icqqElementsToSegments(elements: IcqqMessageElement[] |
|
|
|
125
150
|
export declare function findIcqqNestedMessageSource(root: unknown, maxDepth?: number): IcqqMessageSource | undefined;
|
|
126
151
|
/** 从 oicq MessageEvent.source 解析被引用消息的 message_id */
|
|
127
152
|
export declare function resolveQuoteIdFromIcqqSource(source: unknown): string | undefined;
|
|
128
|
-
/**
|
|
129
|
-
export declare function
|
|
153
|
+
/** Convert an ICQQ source into the adapter-local lookup payload. */
|
|
154
|
+
export declare function messageLookupFromIcqqSource(source: unknown): IcqqMessageLookup | null;
|
|
130
155
|
/** 从事件顶层字段解析引用 message_id(与 message 段互补) */
|
|
131
156
|
export declare function resolveIcqqQuoteIdFromEvent(data: IcqqMessageEvent): string | undefined;
|
|
132
157
|
export declare function resolveInboundContent(data: IcqqMessageEvent): MessageSegment[];
|
|
@@ -137,3 +162,4 @@ export declare class InboundMessageDeduper {
|
|
|
137
162
|
shouldProcess(messageId: string): boolean;
|
|
138
163
|
clear(): void;
|
|
139
164
|
}
|
|
165
|
+
export {};
|
package/lib/icqq-inbound.js
CHANGED
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* icqq 入站事件归一化(post_type / message_type / message 段)
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { toCanonicalSegments } from "zhin.js";
|
|
5
5
|
import { parseCqMessage, icqqMediaRefFromString } from "./cq-message.js";
|
|
6
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
|
+
}
|
|
7
35
|
const DEDUPE_TTL_MS = 120_000;
|
|
8
36
|
const ICQQ_GROUP_ROLES = new Set(["owner", "admin", "member"]);
|
|
9
37
|
/** 从 sender.role 解析群成员角色(供 Message.$sender → resolveSenderRoles) */
|
|
@@ -212,9 +240,21 @@ export function icqqElementsToSegments(elements) {
|
|
|
212
240
|
break;
|
|
213
241
|
case "image": {
|
|
214
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;
|
|
215
255
|
out.push({
|
|
216
256
|
type: "image",
|
|
217
|
-
data: { media:
|
|
257
|
+
data: { media: icqqMediaRefFromElement(value, el), flash: true },
|
|
218
258
|
});
|
|
219
259
|
break;
|
|
220
260
|
}
|
|
@@ -281,18 +321,42 @@ export function icqqElementsToSegments(elements) {
|
|
|
281
321
|
}
|
|
282
322
|
case "record":
|
|
283
323
|
case "audio": {
|
|
284
|
-
const value = String(el.
|
|
324
|
+
const value = String(el.url ?? el.file ?? "");
|
|
325
|
+
if (!value)
|
|
326
|
+
break;
|
|
285
327
|
out.push({
|
|
286
|
-
type: "
|
|
287
|
-
data: {
|
|
328
|
+
type: "audio",
|
|
329
|
+
data: {
|
|
330
|
+
media: icqqMediaRefFromElement(value, el),
|
|
331
|
+
...(typeof el.seconds === "number" ? { duration: el.seconds } : {}),
|
|
332
|
+
},
|
|
288
333
|
});
|
|
289
334
|
break;
|
|
290
335
|
}
|
|
291
336
|
case "video": {
|
|
292
|
-
const value = String(el.
|
|
337
|
+
const value = String(el.url ?? el.file ?? "");
|
|
338
|
+
if (!value)
|
|
339
|
+
break;
|
|
293
340
|
out.push({
|
|
294
341
|
type: "video",
|
|
295
|
-
data: {
|
|
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
|
+
},
|
|
296
360
|
});
|
|
297
361
|
break;
|
|
298
362
|
}
|
|
@@ -305,6 +369,20 @@ export function icqqElementsToSegments(elements) {
|
|
|
305
369
|
}
|
|
306
370
|
return out.length ? out : null;
|
|
307
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
|
+
}
|
|
308
386
|
function resolveReplyIdFromJsonElement(el) {
|
|
309
387
|
let payload = el.data ?? el;
|
|
310
388
|
if (typeof payload === "string") {
|
|
@@ -422,8 +500,8 @@ function icqqSourceToContentSegments(s) {
|
|
|
422
500
|
}
|
|
423
501
|
return [];
|
|
424
502
|
}
|
|
425
|
-
/**
|
|
426
|
-
export function
|
|
503
|
+
/** Convert an ICQQ source into the adapter-local lookup payload. */
|
|
504
|
+
export function messageLookupFromIcqqSource(source) {
|
|
427
505
|
if (!source || typeof source !== "object")
|
|
428
506
|
return null;
|
|
429
507
|
const s = source;
|
|
@@ -481,10 +559,10 @@ export function resolveIcqqQuoteIdFromEvent(data) {
|
|
|
481
559
|
return undefined;
|
|
482
560
|
}
|
|
483
561
|
function mergeReplyFromRawMessage(content, rawMessage) {
|
|
484
|
-
if (!rawMessage ||
|
|
562
|
+
if (!rawMessage || quoteIdFromSegments(content))
|
|
485
563
|
return content;
|
|
486
564
|
const fromRaw = parseCqMessage(rawMessage);
|
|
487
|
-
const quoteFromRaw =
|
|
565
|
+
const quoteFromRaw = quoteIdFromSegments(fromRaw);
|
|
488
566
|
if (!quoteFromRaw)
|
|
489
567
|
return content;
|
|
490
568
|
return [
|
|
@@ -493,7 +571,7 @@ function mergeReplyFromRawMessage(content, rawMessage) {
|
|
|
493
571
|
];
|
|
494
572
|
}
|
|
495
573
|
function mergeReplyFromSource(content, data) {
|
|
496
|
-
if (
|
|
574
|
+
if (quoteIdFromSegments(content))
|
|
497
575
|
return content;
|
|
498
576
|
const quoteId = resolveQuoteIdFromIcqqSource(data.source) ??
|
|
499
577
|
resolveQuoteIdFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
@@ -504,6 +582,11 @@ function mergeReplyFromSource(content, data) {
|
|
|
504
582
|
...content.filter((s) => s.type !== "reply"),
|
|
505
583
|
];
|
|
506
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
|
+
}
|
|
507
590
|
export function resolveInboundContent(data) {
|
|
508
591
|
const fromElements = icqqElementsToSegments(data.message);
|
|
509
592
|
const raw = data.raw_message ?? "";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LoginAssist, LoginAssistType } from '@zhin.js/core';
|
|
2
|
+
type IcqqLoginClient = {
|
|
3
|
+
login(password?: string): Promise<unknown>;
|
|
4
|
+
submitSlider(ticket: string): Promise<unknown>;
|
|
5
|
+
sendSmsCode(): Promise<unknown>;
|
|
6
|
+
submitSmsCode(code: string): Promise<unknown>;
|
|
7
|
+
};
|
|
8
|
+
type Logger = {
|
|
9
|
+
info(message: string): void;
|
|
10
|
+
warn(message: string): void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Drive ICQQ login challenges through LoginAssist (refresh-safe pending tasks).
|
|
14
|
+
* Mirrors the classic stdin flow: qrcode/auth → login(); slider → submitSlider;
|
|
15
|
+
* device → sendSmsCode then submitSmsCode.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runIcqqLoginAssistStep(input: {
|
|
18
|
+
assist: LoginAssist;
|
|
19
|
+
client: IcqqLoginClient;
|
|
20
|
+
adapter: string;
|
|
21
|
+
endpointKey: string;
|
|
22
|
+
owner: object;
|
|
23
|
+
logger: Logger;
|
|
24
|
+
step: LoginAssistType;
|
|
25
|
+
event: unknown;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
export {};
|