@zhin.js/adapter-icqq 8.0.13 → 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 +47 -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/src/forward-msg.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import { segment, type MessageSegment, type QuotedMessagePayload } from 'zhin.js';
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
icqqElementsToSegments,
|
|
5
3
|
type IcqqMessageElement,
|
|
6
4
|
} from "./icqq-inbound.js";
|
|
7
|
-
import {
|
|
5
|
+
import { toCanonicalSegments } from '@zhin.js/core';
|
|
6
|
+
import type { ForwardEntry, Segment } from '@zhin.js/im-contract';
|
|
8
7
|
|
|
9
8
|
const RESID_IN_XML_RE =
|
|
10
9
|
/(?:m_resid|resid|fileid|res_id)=["']?([A-Za-z0-9+/=_.-]{8,})/i;
|
|
11
10
|
|
|
12
|
-
const FORWARD_PLACEHOLDER_RE =
|
|
13
|
-
/\[聊天记录\]|合并转发|转发消息|聊天记录/i;
|
|
14
|
-
|
|
15
11
|
function parseJsonPayload(raw: unknown): Record<string, unknown> | null {
|
|
16
12
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
17
13
|
return raw as Record<string, unknown>;
|
|
@@ -76,344 +72,41 @@ export function extractForwardResidFromJsonElement(
|
|
|
76
72
|
return undefined;
|
|
77
73
|
}
|
|
78
74
|
|
|
79
|
-
/**
|
|
80
|
-
export function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (!text) return undefined;
|
|
85
|
-
|
|
86
|
-
const xml = text.match(RESID_IN_XML_RE);
|
|
87
|
-
if (xml?.[1]) return xml[1].trim();
|
|
88
|
-
|
|
89
|
-
const asJson = parseJsonPayload(text);
|
|
90
|
-
if (asJson) {
|
|
91
|
-
const fromJson = extractForwardResidFromJsonElement({
|
|
92
|
-
type: "json",
|
|
93
|
-
data: asJson,
|
|
94
|
-
});
|
|
95
|
-
if (fromJson) return fromJson;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
for (const match of text.matchAll(/\[CQ:json(?:,([^\]]*))?\]/gi)) {
|
|
99
|
-
const arg = match[1] ?? "";
|
|
100
|
-
const dataMatch = arg.match(/(?:^|,)data=(.+)$/s);
|
|
101
|
-
if (!dataMatch?.[1]) continue;
|
|
102
|
-
const record = parseJsonPayload(dataMatch[1].trim());
|
|
103
|
-
if (!record) continue;
|
|
104
|
-
const fromCq = extractForwardResidFromJsonElement({
|
|
105
|
-
type: "json",
|
|
106
|
-
data: record,
|
|
107
|
-
});
|
|
108
|
-
if (fromCq) return fromCq;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const segs = parseCqMessage(text);
|
|
112
|
-
return extractForwardResidFromSegments(segs);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** 从 get_msg 的 message 元素数组解析 multimsg resid */
|
|
116
|
-
export function extractForwardResidFromGetMsgElements(
|
|
117
|
-
elements: IcqqMessageElement[] | undefined,
|
|
118
|
-
): string | undefined {
|
|
119
|
-
if (!elements?.length) return undefined;
|
|
120
|
-
for (const el of elements) {
|
|
121
|
-
if (!el || typeof el !== "object") continue;
|
|
122
|
-
if (el.type === "forward") {
|
|
123
|
-
const id = String(el.id ?? el.resid ?? el.res_id ?? el.file ?? "").trim();
|
|
124
|
-
if (id) return id;
|
|
125
|
-
}
|
|
126
|
-
const fromJson = extractForwardResidFromJsonElement(el);
|
|
127
|
-
if (fromJson) return fromJson;
|
|
128
|
-
}
|
|
129
|
-
const segs = icqqElementsToSegments(elements);
|
|
130
|
-
if (segs?.length) {
|
|
131
|
-
return extractForwardResidFromSegments(segs);
|
|
132
|
-
}
|
|
133
|
-
return extractForwardResidDeep(elements);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* 从 get_msg 响应解析合并转发 resid(优先 message 数组,其次 raw_message)。
|
|
138
|
-
*/
|
|
139
|
-
export function extractForwardResidFromGetMsg(
|
|
140
|
-
data: unknown,
|
|
141
|
-
): string | undefined {
|
|
142
|
-
if (!data || typeof data !== "object") return undefined;
|
|
143
|
-
const record = data as Record<string, unknown>;
|
|
144
|
-
|
|
145
|
-
if (Array.isArray(record.message)) {
|
|
146
|
-
const fromElements = extractForwardResidFromGetMsgElements(
|
|
147
|
-
record.message as IcqqMessageElement[],
|
|
148
|
-
);
|
|
149
|
-
if (fromElements) return fromElements;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (typeof record.raw_message === "string" && record.raw_message.trim()) {
|
|
153
|
-
const fromRaw = extractForwardResidFromRawMessage(record.raw_message);
|
|
154
|
-
if (fromRaw) return fromRaw;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (typeof record.message === "string" && record.message.trim()) {
|
|
158
|
-
return extractForwardResidFromRawMessage(record.message);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function extractForwardResidFromSegments(
|
|
165
|
-
content: MessageSegment[],
|
|
166
|
-
): string | undefined {
|
|
167
|
-
for (const seg of content) {
|
|
168
|
-
if (seg.type === "forward") {
|
|
169
|
-
const id = seg.data?.id ?? seg.data?.resid;
|
|
170
|
-
if (id != null && String(id).trim()) return String(id).trim();
|
|
171
|
-
}
|
|
172
|
-
if (seg.type === "json" && seg.data) {
|
|
173
|
-
const resid = extractForwardResidFromJsonElement(
|
|
174
|
-
seg.data as IcqqMessageElement,
|
|
175
|
-
);
|
|
176
|
-
if (resid) return resid;
|
|
177
|
-
const raw = seg.data.data ?? seg.data.text;
|
|
178
|
-
const record = parseJsonPayload(raw);
|
|
179
|
-
if (record) {
|
|
180
|
-
const fromEl = extractForwardResidFromJsonElement({
|
|
181
|
-
type: "json",
|
|
182
|
-
data: record,
|
|
183
|
-
});
|
|
184
|
-
if (fromEl) return fromEl;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const raw = segment.raw(content);
|
|
189
|
-
const m = raw.match(RESID_IN_XML_RE);
|
|
190
|
-
return m?.[1]?.trim();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export function extractForwardResidFromPayload(
|
|
194
|
-
payload: QuotedMessagePayload,
|
|
195
|
-
): string | undefined {
|
|
196
|
-
if (Array.isArray(payload.content)) {
|
|
197
|
-
const fromSeg = extractForwardResidFromSegments(
|
|
198
|
-
payload.content as MessageSegment[],
|
|
199
|
-
);
|
|
200
|
-
if (fromSeg) return fromSeg;
|
|
201
|
-
}
|
|
202
|
-
if (typeof payload.raw === "string") {
|
|
203
|
-
const m = payload.raw.match(RESID_IN_XML_RE);
|
|
204
|
-
if (m?.[1]) return m[1].trim();
|
|
205
|
-
const record = parseJsonPayload(payload.raw);
|
|
206
|
-
if (record) {
|
|
207
|
-
return extractForwardResidFromJsonElement({ type: "json", data: record });
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return undefined;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** 在 get_msg 原始对象里深度搜索 resid */
|
|
214
|
-
export function extractForwardResidDeep(
|
|
215
|
-
root: unknown,
|
|
216
|
-
maxDepth = 14,
|
|
217
|
-
): string | undefined {
|
|
218
|
-
const seen = new Set<unknown>();
|
|
219
|
-
function walk(node: unknown, depth: number): string | undefined {
|
|
220
|
-
if (depth > maxDepth || node == null) return undefined;
|
|
221
|
-
if (typeof node === "string") {
|
|
222
|
-
const m = node.match(RESID_IN_XML_RE);
|
|
223
|
-
if (m?.[1]) return m[1].trim();
|
|
224
|
-
const record = parseJsonPayload(node);
|
|
225
|
-
if (record) {
|
|
226
|
-
const fromJson = extractForwardResidFromJsonElement({
|
|
227
|
-
type: "json",
|
|
228
|
-
data: record,
|
|
229
|
-
});
|
|
230
|
-
if (fromJson) return fromJson;
|
|
231
|
-
}
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
if (typeof node !== "object") return undefined;
|
|
235
|
-
if (seen.has(node)) return undefined;
|
|
236
|
-
seen.add(node);
|
|
237
|
-
|
|
238
|
-
if (Array.isArray(node)) {
|
|
239
|
-
for (const item of node) {
|
|
240
|
-
const hit = walk(item, depth + 1);
|
|
241
|
-
if (hit) return hit;
|
|
242
|
-
}
|
|
243
|
-
return undefined;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const fromEl = extractForwardResidFromJsonElement({
|
|
247
|
-
type: "json",
|
|
248
|
-
data: node as IcqqMessageElement,
|
|
249
|
-
});
|
|
250
|
-
if (fromEl) return fromEl;
|
|
251
|
-
|
|
252
|
-
for (const value of Object.values(node as Record<string, unknown>)) {
|
|
253
|
-
const hit = walk(value, depth + 1);
|
|
254
|
-
if (hit) return hit;
|
|
255
|
-
}
|
|
256
|
-
return undefined;
|
|
257
|
-
}
|
|
258
|
-
return walk(root, 0);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function isForwardPlaceholderPayload(
|
|
262
|
-
payload: QuotedMessagePayload,
|
|
263
|
-
): boolean {
|
|
264
|
-
if (
|
|
265
|
-
Array.isArray(payload.content) &&
|
|
266
|
-
payload.content.some((s) => s.type === "forward")
|
|
267
|
-
) {
|
|
268
|
-
return true;
|
|
269
|
-
}
|
|
270
|
-
const raw = Array.isArray(payload.content)
|
|
271
|
-
? segment.raw(payload.content as MessageSegment[])
|
|
272
|
-
: String(payload.content ?? payload.raw ?? "");
|
|
273
|
-
return FORWARD_PLACEHOLDER_RE.test(raw);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function hasMergedForwardBlock(payload: QuotedMessagePayload): boolean {
|
|
277
|
-
const raw = Array.isArray(payload.content)
|
|
278
|
-
? segment.raw(payload.content as MessageSegment[])
|
|
279
|
-
: String(payload.content ?? "");
|
|
280
|
-
return raw.includes("[Merged chat history");
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function senderLabel(sender: unknown): string {
|
|
284
|
-
if (!sender || typeof sender !== "object") return "unknown";
|
|
285
|
-
const s = sender as Record<string, unknown>;
|
|
286
|
-
const name = s.nickname ?? s.name ?? s.card;
|
|
287
|
-
const id = s.user_id ?? s.uin ?? s.uid ?? s.id;
|
|
288
|
-
if (name && id) return `${name} (${id})`;
|
|
289
|
-
if (name) return String(name);
|
|
290
|
-
if (id != null) return String(id);
|
|
291
|
-
return "unknown";
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function messageBodyToText(body: unknown): string {
|
|
295
|
-
if (body == null) return "";
|
|
296
|
-
if (typeof body === "string") return body.trim();
|
|
297
|
-
if (Array.isArray(body)) {
|
|
298
|
-
const segs = icqqElementsToSegments(body as IcqqMessageElement[]);
|
|
299
|
-
return segs?.length ? segment.raw(segs).trim() : segment.raw(body).trim();
|
|
300
|
-
}
|
|
301
|
-
if (typeof body === "object") {
|
|
302
|
-
const r = body as Record<string, unknown>;
|
|
303
|
-
if (typeof r.raw_message === "string" && r.raw_message.trim()) {
|
|
304
|
-
return r.raw_message.trim();
|
|
305
|
-
}
|
|
306
|
-
if (Array.isArray(r.message)) {
|
|
307
|
-
return messageBodyToText(r.message);
|
|
308
|
-
}
|
|
309
|
-
if (Array.isArray(r.elements)) {
|
|
310
|
-
return messageBodyToText(r.elements);
|
|
311
|
-
}
|
|
312
|
-
if (typeof r.content === "string") return r.content.trim();
|
|
313
|
-
}
|
|
314
|
-
return "";
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** 将 get_forward_msg 响应格式化为可读聊天记录 */
|
|
318
|
-
export function formatForwardMsgResponse(data: unknown): string {
|
|
319
|
-
if (!data) return "";
|
|
320
|
-
|
|
321
|
-
const root =
|
|
322
|
-
data && typeof data === "object" && !Array.isArray(data)
|
|
323
|
-
? (data as Record<string, unknown>)
|
|
324
|
-
: null;
|
|
325
|
-
|
|
75
|
+
/** Canonical merged-forward entries. Speakers are data actors, never LLM roles. */
|
|
76
|
+
export function normalizeForwardMsgResponse(data: unknown): readonly ForwardEntry[] {
|
|
77
|
+
const root = data && typeof data === 'object' && !Array.isArray(data)
|
|
78
|
+
? data as Record<string, unknown>
|
|
79
|
+
: undefined;
|
|
326
80
|
const list: unknown[] = Array.isArray(data)
|
|
327
81
|
? data
|
|
328
|
-
: Array.isArray(root?.messages)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
: Array.isArray(root?.msg_list)
|
|
333
|
-
? (root!.msg_list as unknown[])
|
|
334
|
-
: Array.isArray(root?.message)
|
|
335
|
-
? (root!.message as unknown[])
|
|
82
|
+
: Array.isArray(root?.messages) ? root.messages as unknown[]
|
|
83
|
+
: Array.isArray(root?.msgList) ? root.msgList as unknown[]
|
|
84
|
+
: Array.isArray(root?.msg_list) ? root.msg_list as unknown[]
|
|
85
|
+
: Array.isArray(root?.message) ? root.message as unknown[]
|
|
336
86
|
: [];
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const lines: string[] = [];
|
|
341
|
-
let index = 0;
|
|
342
|
-
for (const item of list) {
|
|
343
|
-
if (!item || typeof item !== "object") continue;
|
|
87
|
+
return Object.freeze(list.flatMap((item): ForwardEntry[] => {
|
|
88
|
+
if (!item || typeof item !== 'object') return [];
|
|
344
89
|
const row = item as Record<string, unknown>;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
): Promise<string> {
|
|
368
|
-
try {
|
|
369
|
-
const messages = await client.getForwardMsg(id);
|
|
370
|
-
const text = formatForwardMsgResponse(messages);
|
|
371
|
-
if (text.trim()) return text.trim();
|
|
372
|
-
} catch { /* ignore */ }
|
|
373
|
-
return "";
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function appendForwardBlock(
|
|
377
|
-
payload: QuotedMessagePayload,
|
|
378
|
-
block: string,
|
|
379
|
-
): QuotedMessagePayload {
|
|
380
|
-
if (Array.isArray(payload.content)) {
|
|
381
|
-
return {
|
|
382
|
-
...payload,
|
|
383
|
-
content: [...payload.content, { type: "text", data: { text: block } }],
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
const prev =
|
|
387
|
-
typeof payload.content === "string" ? payload.content.trim() : "";
|
|
388
|
-
return {
|
|
389
|
-
...payload,
|
|
390
|
-
content: prev ? `${prev}\n\n${block}` : block,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export async function enrichQuotedPayloadWithForward(
|
|
395
|
-
client: { getForwardMsg(resId: string, fileName?: string): Promise<unknown[]> } | null | undefined,
|
|
396
|
-
payload: QuotedMessagePayload,
|
|
397
|
-
rawData?: unknown,
|
|
398
|
-
): Promise<QuotedMessagePayload> {
|
|
399
|
-
if (!client || hasMergedForwardBlock(payload)) return payload;
|
|
400
|
-
|
|
401
|
-
const resid =
|
|
402
|
-
extractForwardResidFromPayload(payload) ??
|
|
403
|
-
(rawData ? extractForwardResidFromGetMsg(rawData) : undefined) ??
|
|
404
|
-
(rawData ? extractForwardResidDeep(rawData) : undefined);
|
|
405
|
-
|
|
406
|
-
const fetchIds = [
|
|
407
|
-
resid,
|
|
408
|
-
isForwardPlaceholderPayload(payload) ? payload.messageId : undefined,
|
|
409
|
-
].filter((v, i, a): v is string => !!v && a.indexOf(v) === i);
|
|
410
|
-
|
|
411
|
-
for (const fetchId of fetchIds) {
|
|
412
|
-
const forwardText = await fetchForwardMsgText(client, fetchId);
|
|
413
|
-
if (!forwardText.trim()) continue;
|
|
414
|
-
const block = `[Merged chat history — id ${fetchId}]\n${forwardText.trim()}`;
|
|
415
|
-
return appendForwardBlock(payload, block);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return payload;
|
|
90
|
+
const sender = row.sender && typeof row.sender === 'object'
|
|
91
|
+
? row.sender as Record<string, unknown>
|
|
92
|
+
: row.user && typeof row.user === 'object'
|
|
93
|
+
? row.user as Record<string, unknown>
|
|
94
|
+
: undefined;
|
|
95
|
+
const id = sender?.user_id ?? sender?.uin ?? sender?.id;
|
|
96
|
+
const displayName = sender?.nickname ?? sender?.card ?? sender?.name;
|
|
97
|
+
const body = row.message ?? row.content ?? row.elements ?? row.raw_message;
|
|
98
|
+
let segments: Segment[];
|
|
99
|
+
if (Array.isArray(body)) segments = toCanonicalSegments(icqqElementsToSegments(body as IcqqMessageElement[]) ?? body);
|
|
100
|
+
else if (typeof body === 'string' && body.trim()) segments = [{ type: 'text', data: { text: body.trim() } }];
|
|
101
|
+
else segments = [];
|
|
102
|
+
if (segments.length === 0) return [];
|
|
103
|
+
return [Object.freeze({
|
|
104
|
+
...(id != null ? { actor: Object.freeze({
|
|
105
|
+
id: String(id),
|
|
106
|
+
...(displayName != null && String(displayName).trim() ? { displayName: String(displayName) } : {}),
|
|
107
|
+
}) } : {}),
|
|
108
|
+
...(typeof row.time === 'number' ? { timestamp: row.time < 1e12 ? row.time * 1000 : row.time } : {}),
|
|
109
|
+
segments: Object.freeze(segments),
|
|
110
|
+
})];
|
|
111
|
+
}));
|
|
419
112
|
}
|
package/src/icqq-inbound.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* icqq 入站事件归一化(post_type / message_type / message 段)
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { toCanonicalSegments, type MessageSegment } from "zhin.js";
|
|
5
5
|
import { parseCqMessage, icqqMediaRefFromString } from "./cq-message.js";
|
|
6
6
|
import { extractForwardResidFromJsonElement } from "./forward-msg.js";
|
|
7
7
|
import type { GroupRole } from "./types.js";
|
|
@@ -66,6 +66,14 @@ export interface IcqqMessageSource {
|
|
|
66
66
|
[key: string]: unknown;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
interface IcqqMessageLookup {
|
|
70
|
+
readonly messageId: string;
|
|
71
|
+
readonly sender?: Readonly<{ id?: string; name?: string }>;
|
|
72
|
+
readonly content: readonly MessageSegment[] | string;
|
|
73
|
+
readonly raw?: string;
|
|
74
|
+
readonly time?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
export type IcqqMessageElement = {
|
|
70
78
|
type: string;
|
|
71
79
|
text?: string;
|
|
@@ -74,9 +82,45 @@ export type IcqqMessageElement = {
|
|
|
74
82
|
id?: string | number;
|
|
75
83
|
url?: string;
|
|
76
84
|
file?: string;
|
|
85
|
+
fid?: string;
|
|
86
|
+
name?: string;
|
|
87
|
+
size?: number;
|
|
88
|
+
seconds?: number;
|
|
77
89
|
[key: string]: unknown;
|
|
78
90
|
};
|
|
79
91
|
|
|
92
|
+
export interface IcqqInboundMediaResolver {
|
|
93
|
+
getPttUrl(element: IcqqMessageElement): Promise<string | null | undefined>;
|
|
94
|
+
getVideoUrl(element: IcqqMessageElement): Promise<string | null | undefined>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve ICQQ's opaque protobuf media references while the endpoint-owned
|
|
99
|
+
* native client is still available. Resolution failure preserves the opaque
|
|
100
|
+
* reference, so canonical ingress stays truthful and can report unsupported
|
|
101
|
+
* media instead of inventing a URL.
|
|
102
|
+
*/
|
|
103
|
+
export async function resolveIcqqInboundMedia(
|
|
104
|
+
elements: IcqqMessageElement[] | undefined,
|
|
105
|
+
resolver: IcqqInboundMediaResolver,
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
if (!elements?.length) return;
|
|
108
|
+
await Promise.all(elements.map(async (element) => {
|
|
109
|
+
if (!element || typeof element !== "object") return;
|
|
110
|
+
if (typeof element.url === "string" && element.url) return;
|
|
111
|
+
try {
|
|
112
|
+
const url = element.type === "record" || element.type === "audio"
|
|
113
|
+
? await resolver.getPttUrl(element)
|
|
114
|
+
: element.type === "video"
|
|
115
|
+
? await resolver.getVideoUrl(element)
|
|
116
|
+
: undefined;
|
|
117
|
+
if (typeof url === "string" && url) element.url = url;
|
|
118
|
+
} catch {
|
|
119
|
+
// The original platform reference remains the canonical fallback.
|
|
120
|
+
}
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
80
124
|
/** 归一化后供适配器使用的入站消息 */
|
|
81
125
|
export interface NormalizedIcqqInbound {
|
|
82
126
|
messageId: string;
|
|
@@ -343,9 +387,19 @@ export function icqqElementsToSegments(
|
|
|
343
387
|
break;
|
|
344
388
|
case "image": {
|
|
345
389
|
const value = String(el.url ?? el.file ?? "");
|
|
390
|
+
if (!value) break;
|
|
391
|
+
out.push({
|
|
392
|
+
type: "image",
|
|
393
|
+
data: { media: icqqMediaRefFromElement(value, el) },
|
|
394
|
+
});
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
case "flash": {
|
|
398
|
+
const value = String(el.url ?? el.file ?? "");
|
|
399
|
+
if (!value) break;
|
|
346
400
|
out.push({
|
|
347
401
|
type: "image",
|
|
348
|
-
data: { media:
|
|
402
|
+
data: { media: icqqMediaRefFromElement(value, el), flash: true },
|
|
349
403
|
});
|
|
350
404
|
break;
|
|
351
405
|
}
|
|
@@ -410,18 +464,39 @@ export function icqqElementsToSegments(
|
|
|
410
464
|
}
|
|
411
465
|
case "record":
|
|
412
466
|
case "audio": {
|
|
413
|
-
const value = String(el.
|
|
467
|
+
const value = String(el.url ?? el.file ?? "");
|
|
468
|
+
if (!value) break;
|
|
414
469
|
out.push({
|
|
415
|
-
type: "
|
|
416
|
-
data: {
|
|
470
|
+
type: "audio",
|
|
471
|
+
data: {
|
|
472
|
+
media: icqqMediaRefFromElement(value, el),
|
|
473
|
+
...(typeof el.seconds === "number" ? { duration: el.seconds } : {}),
|
|
474
|
+
},
|
|
417
475
|
});
|
|
418
476
|
break;
|
|
419
477
|
}
|
|
420
478
|
case "video": {
|
|
421
|
-
const value = String(el.
|
|
479
|
+
const value = String(el.url ?? el.file ?? "");
|
|
480
|
+
if (!value) break;
|
|
422
481
|
out.push({
|
|
423
482
|
type: "video",
|
|
424
|
-
data: {
|
|
483
|
+
data: {
|
|
484
|
+
media: icqqMediaRefFromElement(value, el),
|
|
485
|
+
...(typeof el.seconds === "number" ? { duration: el.seconds } : {}),
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case "file": {
|
|
491
|
+
const value = String(el.url ?? el.file ?? el.fid ?? "");
|
|
492
|
+
if (!value) break;
|
|
493
|
+
const name = typeof el.name === "string" && el.name ? el.name : undefined;
|
|
494
|
+
out.push({
|
|
495
|
+
type: "file",
|
|
496
|
+
data: {
|
|
497
|
+
media: icqqMediaRefFromElement(value, el),
|
|
498
|
+
...(name ? { name } : {}),
|
|
499
|
+
},
|
|
425
500
|
});
|
|
426
501
|
break;
|
|
427
502
|
}
|
|
@@ -435,6 +510,24 @@ export function icqqElementsToSegments(
|
|
|
435
510
|
return out.length ? out : null;
|
|
436
511
|
}
|
|
437
512
|
|
|
513
|
+
function icqqMediaRefFromElement(
|
|
514
|
+
value: string,
|
|
515
|
+
element: IcqqMessageElement,
|
|
516
|
+
): ReturnType<typeof icqqMediaRefFromString> {
|
|
517
|
+
const media = icqqMediaRefFromString(value);
|
|
518
|
+
const fileName = typeof element.name === "string" && element.name
|
|
519
|
+
? element.name
|
|
520
|
+
: undefined;
|
|
521
|
+
const size = typeof element.size === "number" && Number.isFinite(element.size)
|
|
522
|
+
? element.size
|
|
523
|
+
: undefined;
|
|
524
|
+
return {
|
|
525
|
+
...media,
|
|
526
|
+
...(fileName ? { file_name: fileName } : {}),
|
|
527
|
+
...(size != null ? { size } : {}),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
438
531
|
function resolveReplyIdFromJsonElement(el: IcqqMessageElement): string | undefined {
|
|
439
532
|
let payload: unknown = el.data ?? el;
|
|
440
533
|
if (typeof payload === "string") {
|
|
@@ -556,10 +649,10 @@ function icqqSourceToContentSegments(
|
|
|
556
649
|
return [];
|
|
557
650
|
}
|
|
558
651
|
|
|
559
|
-
/**
|
|
560
|
-
export function
|
|
652
|
+
/** Convert an ICQQ source into the adapter-local lookup payload. */
|
|
653
|
+
export function messageLookupFromIcqqSource(
|
|
561
654
|
source: unknown,
|
|
562
|
-
):
|
|
655
|
+
): IcqqMessageLookup | null {
|
|
563
656
|
if (!source || typeof source !== "object") return null;
|
|
564
657
|
const s = source as IcqqMessageSource;
|
|
565
658
|
const body = icqqSourceMessageBody(s);
|
|
@@ -622,9 +715,9 @@ function mergeReplyFromRawMessage(
|
|
|
622
715
|
content: MessageSegment[],
|
|
623
716
|
rawMessage: string,
|
|
624
717
|
): MessageSegment[] {
|
|
625
|
-
if (!rawMessage ||
|
|
718
|
+
if (!rawMessage || quoteIdFromSegments(content)) return content;
|
|
626
719
|
const fromRaw = parseCqMessage(rawMessage);
|
|
627
|
-
const quoteFromRaw =
|
|
720
|
+
const quoteFromRaw = quoteIdFromSegments(fromRaw);
|
|
628
721
|
if (!quoteFromRaw) return content;
|
|
629
722
|
return [
|
|
630
723
|
{ type: "reply", data: { message_id: quoteFromRaw } },
|
|
@@ -636,7 +729,7 @@ function mergeReplyFromSource(
|
|
|
636
729
|
content: MessageSegment[],
|
|
637
730
|
data: IcqqMessageEvent,
|
|
638
731
|
): MessageSegment[] {
|
|
639
|
-
if (
|
|
732
|
+
if (quoteIdFromSegments(content)) return content;
|
|
640
733
|
const quoteId =
|
|
641
734
|
resolveQuoteIdFromIcqqSource(data.source) ??
|
|
642
735
|
resolveQuoteIdFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
@@ -647,6 +740,12 @@ function mergeReplyFromSource(
|
|
|
647
740
|
];
|
|
648
741
|
}
|
|
649
742
|
|
|
743
|
+
function quoteIdFromSegments(content: readonly MessageSegment[]): string | undefined {
|
|
744
|
+
const reply = content.find((item) => item.type === 'reply');
|
|
745
|
+
const value = reply?.data?.message_id ?? reply?.data?.id;
|
|
746
|
+
return value == null || !String(value).trim() ? undefined : String(value).trim();
|
|
747
|
+
}
|
|
748
|
+
|
|
650
749
|
export function resolveInboundContent(
|
|
651
750
|
data: IcqqMessageEvent,
|
|
652
751
|
): MessageSegment[] {
|