@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { formatCompact } from '@zhin.js/logger';
|
|
2
|
+
/**
|
|
3
|
+
* Drive ICQQ login challenges through LoginAssist (refresh-safe pending tasks).
|
|
4
|
+
* Mirrors the classic stdin flow: qrcode/auth → login(); slider → submitSlider;
|
|
5
|
+
* device → sendSmsCode then submitSmsCode.
|
|
6
|
+
*/
|
|
7
|
+
export async function runIcqqLoginAssistStep(input) {
|
|
8
|
+
const { assist, client, adapter, endpointKey, owner, logger, step, event } = input;
|
|
9
|
+
assist.cancelOwned(owner, `replaced_by_${step}`);
|
|
10
|
+
const payload = buildPayload(step, event);
|
|
11
|
+
logger.info(formatCompact({
|
|
12
|
+
op: 'login_assist_pending',
|
|
13
|
+
endpoint: endpointKey,
|
|
14
|
+
type: step,
|
|
15
|
+
message: payload.message,
|
|
16
|
+
...(typeof payload.url === 'string' ? { url: payload.url } : {}),
|
|
17
|
+
}));
|
|
18
|
+
try {
|
|
19
|
+
if (step === 'device') {
|
|
20
|
+
try {
|
|
21
|
+
await client.sendSmsCode();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
logger.warn(formatCompact({
|
|
25
|
+
op: 'login_assist_send_sms_failed',
|
|
26
|
+
endpoint: endpointKey,
|
|
27
|
+
error: error instanceof Error ? error.message : String(error),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const value = await assist.waitForInput(adapter, endpointKey, step, payload, { owner });
|
|
32
|
+
const text = normalizeSubmitValue(value);
|
|
33
|
+
switch (step) {
|
|
34
|
+
case 'qrcode':
|
|
35
|
+
case 'auth':
|
|
36
|
+
await client.login();
|
|
37
|
+
break;
|
|
38
|
+
case 'slider':
|
|
39
|
+
await client.submitSlider(text);
|
|
40
|
+
break;
|
|
41
|
+
case 'device':
|
|
42
|
+
case 'sms':
|
|
43
|
+
await client.submitSmsCode(text);
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
await client.login();
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
logger.warn(formatCompact({
|
|
52
|
+
op: 'login_assist_failed',
|
|
53
|
+
endpoint: endpointKey,
|
|
54
|
+
type: step,
|
|
55
|
+
error: error instanceof Error ? error.message : String(error),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function buildPayload(step, event) {
|
|
60
|
+
const raw = event && typeof event === 'object' ? event : {};
|
|
61
|
+
switch (step) {
|
|
62
|
+
case 'qrcode':
|
|
63
|
+
return {
|
|
64
|
+
message: '扫码登录:请在手机 QQ 扫描二维码,完成后在 Console / 终端确认继续',
|
|
65
|
+
image: bufferToDataUrl(raw.image),
|
|
66
|
+
};
|
|
67
|
+
case 'slider':
|
|
68
|
+
return {
|
|
69
|
+
message: '滑动验证:打开链接完成滑块,将返回的 ticket(可含 randstr)提交继续',
|
|
70
|
+
url: typeof raw.url === 'string' ? raw.url : undefined,
|
|
71
|
+
};
|
|
72
|
+
case 'device':
|
|
73
|
+
return {
|
|
74
|
+
message: '设备锁:已请求短信验证码,请提交短信验证码继续',
|
|
75
|
+
url: typeof raw.url === 'string' ? raw.url : undefined,
|
|
76
|
+
phone: raw.phone,
|
|
77
|
+
};
|
|
78
|
+
case 'auth':
|
|
79
|
+
return {
|
|
80
|
+
message: '身份验证:完成后在 Console / 终端确认继续',
|
|
81
|
+
url: typeof raw.url === 'string' ? raw.url : undefined,
|
|
82
|
+
device: raw.device,
|
|
83
|
+
};
|
|
84
|
+
default:
|
|
85
|
+
return { message: `登录辅助(${step})` };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function normalizeSubmitValue(value) {
|
|
89
|
+
if (typeof value === 'string')
|
|
90
|
+
return value.trim();
|
|
91
|
+
if (typeof value.ticket === 'string')
|
|
92
|
+
return value.ticket.trim();
|
|
93
|
+
if (typeof value.code === 'string')
|
|
94
|
+
return value.code.trim();
|
|
95
|
+
if (typeof value.value === 'string')
|
|
96
|
+
return value.value.trim();
|
|
97
|
+
return JSON.stringify(value);
|
|
98
|
+
}
|
|
99
|
+
function bufferToDataUrl(image) {
|
|
100
|
+
if (Buffer.isBuffer(image)) {
|
|
101
|
+
return `data:image/png;base64,${image.toString('base64')}`;
|
|
102
|
+
}
|
|
103
|
+
if (image instanceof Uint8Array) {
|
|
104
|
+
return `data:image/png;base64,${Buffer.from(image).toString('base64')}`;
|
|
105
|
+
}
|
|
106
|
+
if (typeof image === 'string' && image.trim())
|
|
107
|
+
return image.trim();
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, type ActionResult, type IcqqAdapterConfig, type IcqqInboundMessage, type IcqqWireSegment, type ParsedIcqqSendTarget, type ResolvedIcqqConfig, } from './protocol.js';
|
|
2
|
-
export { IcqqEndpoint, type IcqqEndpointOptions,
|
|
2
|
+
export { IcqqEndpoint, type IcqqEndpointOptions, } from './endpoint.js';
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export { getIcqqAgentDeps, registerIcqqAgentEndpoint, setIcqqAgentDeps, type IcqqAgentDeps, type IcqqAgentEndpoint, } from './icqq-agent-deps.js';
|
|
5
5
|
export { resolveIcqqInboundMessageId, } from './icqq-inbound.js';
|
package/lib/outbound-media.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ export declare function resolveIcqqOutboundMediaMode(config: Pick<IcqqEndpointCo
|
|
|
5
5
|
/**
|
|
6
6
|
* file 模式:将 canonical MediaRef(`data.media`,kind=base64)物化为本机路径,
|
|
7
7
|
* 并把 media 改写为 path 引用(icqq 客户端直接读盘)。
|
|
8
|
-
* base64 模式:不改动(由
|
|
8
|
+
* base64 模式:不改动(由 native Sendable projection 生成 base64:// file)。
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
10
|
+
export interface PreparedIcqqOutboundMedia {
|
|
11
|
+
readonly content: SendContent;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function prepareIcqqOutboundMedia(content: SendContent, mode?: IcqqOutboundMediaMode): PreparedIcqqOutboundMedia;
|
package/lib/outbound-media.js
CHANGED
|
@@ -29,19 +29,18 @@ export function resolveIcqqOutboundMediaMode(config) {
|
|
|
29
29
|
}
|
|
30
30
|
return "file";
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
* file 模式:将 canonical MediaRef(`data.media`,kind=base64)物化为本机路径,
|
|
34
|
-
* 并把 media 改写为 path 引用(icqq 客户端直接读盘)。
|
|
35
|
-
* base64 模式:不改动(由 toCqString/formatOutboundBody 生成 base64://)。
|
|
36
|
-
*/
|
|
37
|
-
export function materializeOutboundBase64(content, mode = "file") {
|
|
32
|
+
export function prepareIcqqOutboundMedia(content, mode = "file") {
|
|
38
33
|
if (mode === "base64")
|
|
39
|
-
return content;
|
|
34
|
+
return { content, dispose() { } };
|
|
35
|
+
const createdFiles = [];
|
|
36
|
+
const inputWasArray = Array.isArray(content);
|
|
40
37
|
const segments = Array.isArray(content) ? content : [content];
|
|
41
|
-
|
|
38
|
+
const materialized = segments.map((seg) => {
|
|
42
39
|
if (typeof seg === "string")
|
|
43
40
|
return seg;
|
|
44
41
|
const { type, data } = seg;
|
|
42
|
+
if (!data || typeof data !== "object")
|
|
43
|
+
return seg;
|
|
45
44
|
const d = data;
|
|
46
45
|
const media = d.media;
|
|
47
46
|
if (!isMediaRef(media) || media.kind !== "base64")
|
|
@@ -54,6 +53,7 @@ export function materializeOutboundBase64(content, mode = "file") {
|
|
|
54
53
|
}
|
|
55
54
|
const mime = media.mime_type ?? "";
|
|
56
55
|
const filePath = spoolBase64ToFile(b64, mime || (type === "image" ? "image/jpeg" : type === "video" ? "video/mp4" : "audio/mpeg"), type === "record" || type === "audio" ? "audio" : type);
|
|
56
|
+
createdFiles.push(filePath);
|
|
57
57
|
return {
|
|
58
58
|
type,
|
|
59
59
|
data: {
|
|
@@ -66,4 +66,11 @@ export function materializeOutboundBase64(content, mode = "file") {
|
|
|
66
66
|
},
|
|
67
67
|
};
|
|
68
68
|
});
|
|
69
|
+
return {
|
|
70
|
+
content: inputWasArray ? materialized : materialized[0],
|
|
71
|
+
dispose() {
|
|
72
|
+
for (const filePath of createdFiles)
|
|
73
|
+
fs.rmSync(filePath, { force: true });
|
|
74
|
+
},
|
|
75
|
+
};
|
|
69
76
|
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Sendable } from '@icqqjs/icqq';
|
|
1
2
|
import type { ConversationRef } from '@zhin.js/im-contract';
|
|
2
3
|
import type { MessageSegment } from "zhin.js";
|
|
3
4
|
export type ActionResult = {
|
|
@@ -231,6 +232,9 @@ export interface IcqqInboundMessage {
|
|
|
231
232
|
};
|
|
232
233
|
readonly channelType: 'private' | 'group' | 'channel';
|
|
233
234
|
readonly metadata?: Record<string, unknown>;
|
|
235
|
+
readonly replyTo?: {
|
|
236
|
+
readonly id: string;
|
|
237
|
+
};
|
|
234
238
|
}
|
|
235
239
|
export type ParsedIcqqSendTarget = {
|
|
236
240
|
readonly kind: 'private';
|
|
@@ -263,5 +267,10 @@ export declare function icqqInboundConversation(endpointKey: string, input: {
|
|
|
263
267
|
* 群容器(parent kind 'group')内的 private 会话即 ICQQ 群临时会话(temp)。
|
|
264
268
|
*/
|
|
265
269
|
export declare function icqqOutboundTarget(conversation: ConversationRef): ParsedIcqqSendTarget;
|
|
266
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Project canonical Zhin segments directly into the native ICQQ Sendable
|
|
272
|
+
* algebra. ICQQ's direct client does not parse Zhin's historical `[image:x]`
|
|
273
|
+
* notation; passing that notation would send it as literal text.
|
|
274
|
+
*/
|
|
275
|
+
export declare function formatOutboundBody(payload: unknown): Sendable;
|
|
267
276
|
export declare function formatInboundContent(rawMessage: string): string;
|
package/lib/protocol.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pickCredential } from 'zhin.js/adapter';
|
|
2
|
-
import {
|
|
2
|
+
import { resolveIcqqMediaFile } from "./cq-message.js";
|
|
3
3
|
export { resolveIcqqInboundMessageId } from "./icqq-inbound.js";
|
|
4
4
|
/**
|
|
5
5
|
* 所有操作常量(@icqqjs/icqq)
|
|
@@ -257,6 +257,18 @@ export function icqqOutboundTarget(conversation) {
|
|
|
257
257
|
}
|
|
258
258
|
return { kind: 'channel', guildId, channelId: conversation.id };
|
|
259
259
|
}
|
|
260
|
+
const ICQQ_STANDALONE_ELEMENTS = new Set([
|
|
261
|
+
'record',
|
|
262
|
+
'video',
|
|
263
|
+
'file',
|
|
264
|
+
'json',
|
|
265
|
+
'long_msg',
|
|
266
|
+
]);
|
|
267
|
+
/**
|
|
268
|
+
* Project canonical Zhin segments directly into the native ICQQ Sendable
|
|
269
|
+
* algebra. ICQQ's direct client does not parse Zhin's historical `[image:x]`
|
|
270
|
+
* notation; passing that notation would send it as literal text.
|
|
271
|
+
*/
|
|
260
272
|
export function formatOutboundBody(payload) {
|
|
261
273
|
if (typeof payload === 'string') {
|
|
262
274
|
const text = payload.trim();
|
|
@@ -276,7 +288,7 @@ export function formatOutboundBody(payload) {
|
|
|
276
288
|
const text = String(payload).trim();
|
|
277
289
|
return text || '\u200b';
|
|
278
290
|
}
|
|
279
|
-
const
|
|
291
|
+
const elements = payload.flatMap((seg) => {
|
|
280
292
|
if (typeof seg === 'string')
|
|
281
293
|
return seg;
|
|
282
294
|
const item = seg;
|
|
@@ -284,32 +296,82 @@ export function formatOutboundBody(payload) {
|
|
|
284
296
|
case 'text':
|
|
285
297
|
return String(item.data?.text ?? '');
|
|
286
298
|
case 'at':
|
|
287
|
-
case 'mention':
|
|
288
|
-
|
|
299
|
+
case 'mention': {
|
|
300
|
+
const target = item.data?.qq ?? item.data?.id ?? item.data?.target;
|
|
301
|
+
if (target === 'all')
|
|
302
|
+
return { type: 'at', qq: 'all' };
|
|
303
|
+
const qq = Number(target);
|
|
304
|
+
if (!Number.isSafeInteger(qq) || qq <= 0) {
|
|
305
|
+
throw new TypeError(`Invalid ICQQ mention target: ${String(target ?? '')}`);
|
|
306
|
+
}
|
|
307
|
+
return { type: 'at', qq };
|
|
308
|
+
}
|
|
289
309
|
case 'image': {
|
|
290
|
-
const arg =
|
|
291
|
-
|
|
310
|
+
const arg = resolveIcqqMediaFile(item.data);
|
|
311
|
+
if (!arg)
|
|
312
|
+
throw new TypeError('ICQQ image segment requires canonical data.media');
|
|
313
|
+
return { type: 'image', file: arg };
|
|
314
|
+
}
|
|
315
|
+
case 'face': {
|
|
316
|
+
const id = Number(item.data?.id);
|
|
317
|
+
if (!Number.isSafeInteger(id) || id < 0) {
|
|
318
|
+
throw new TypeError(`Invalid ICQQ face id: ${String(item.data?.id ?? '')}`);
|
|
319
|
+
}
|
|
320
|
+
return { type: 'face', id };
|
|
321
|
+
}
|
|
322
|
+
case 'reply': {
|
|
323
|
+
const id = String(item.data?.message_id ?? item.data?.id ?? '').trim();
|
|
324
|
+
if (!id)
|
|
325
|
+
throw new TypeError('ICQQ reply segment requires message_id');
|
|
326
|
+
return { type: 'reply', id };
|
|
292
327
|
}
|
|
293
|
-
case 'face':
|
|
294
|
-
return `[face:${item.data?.id ?? ''}]`;
|
|
295
|
-
case 'reply':
|
|
296
|
-
return `[reply:${item.data?.message_id ?? item.data?.id ?? ''}]`;
|
|
297
328
|
case 'record':
|
|
298
329
|
case 'audio': {
|
|
299
|
-
const arg =
|
|
300
|
-
|
|
330
|
+
const arg = resolveIcqqMediaFile(item.data);
|
|
331
|
+
if (!arg)
|
|
332
|
+
throw new TypeError('ICQQ audio segment requires canonical data.media');
|
|
333
|
+
return { type: 'record', file: arg };
|
|
301
334
|
}
|
|
302
335
|
case 'video': {
|
|
303
|
-
const arg =
|
|
304
|
-
|
|
336
|
+
const arg = resolveIcqqMediaFile(item.data);
|
|
337
|
+
if (!arg)
|
|
338
|
+
throw new TypeError('ICQQ video segment requires canonical data.media');
|
|
339
|
+
return { type: 'video', file: arg };
|
|
340
|
+
}
|
|
341
|
+
case 'file': {
|
|
342
|
+
const arg = resolveIcqqMediaFile(item.data);
|
|
343
|
+
if (!arg)
|
|
344
|
+
throw new TypeError('ICQQ file segment requires canonical data.media');
|
|
345
|
+
const name = String(item.data?.name ?? '').trim();
|
|
346
|
+
return { type: 'file', file: arg, ...(name ? { name } : {}) };
|
|
347
|
+
}
|
|
348
|
+
case 'dice':
|
|
349
|
+
return { type: 'dice' };
|
|
350
|
+
case 'rps':
|
|
351
|
+
return { type: 'rps' };
|
|
352
|
+
case 'forward': {
|
|
353
|
+
const resid = String(item.data?.forward_id ?? item.data?.resid ?? item.data?.id ?? '').trim();
|
|
354
|
+
if (!resid)
|
|
355
|
+
throw new TypeError('ICQQ forward segment requires forward_id');
|
|
356
|
+
return { type: 'long_msg', resid };
|
|
305
357
|
}
|
|
306
358
|
case 'share':
|
|
307
359
|
return formatMusicShareJson(item.data);
|
|
308
|
-
default:
|
|
309
|
-
|
|
360
|
+
default: {
|
|
361
|
+
const text = item.data?.text;
|
|
362
|
+
if (typeof text === 'string' && text)
|
|
363
|
+
return text;
|
|
364
|
+
throw new TypeError(`Unsupported ICQQ outbound segment: ${item.type}`);
|
|
365
|
+
}
|
|
310
366
|
}
|
|
311
|
-
}).
|
|
312
|
-
|
|
367
|
+
}).filter((element) => typeof element !== 'string' || element.length > 0);
|
|
368
|
+
if (elements.length === 0)
|
|
369
|
+
return '\u200b';
|
|
370
|
+
const standalone = elements.find((element) => typeof element !== 'string' && ICQQ_STANDALONE_ELEMENTS.has(element.type));
|
|
371
|
+
if (standalone && elements.length !== 1) {
|
|
372
|
+
throw new TypeError(`ICQQ ${standalone.type} element must be sent as a standalone message`);
|
|
373
|
+
}
|
|
374
|
+
return elements.length === 1 ? elements[0] : elements;
|
|
313
375
|
}
|
|
314
376
|
export function formatInboundContent(rawMessage) {
|
|
315
377
|
return rawMessage;
|
|
@@ -322,7 +384,7 @@ function isWireSegment(value) {
|
|
|
322
384
|
}
|
|
323
385
|
function formatMusicShareJson(data) {
|
|
324
386
|
if (!data)
|
|
325
|
-
|
|
387
|
+
throw new TypeError('ICQQ share segment requires data');
|
|
326
388
|
const config = data.config;
|
|
327
389
|
const title = String(data.title ?? '');
|
|
328
390
|
const desc = String(data.artist ?? '');
|
|
@@ -359,5 +421,5 @@ function formatMusicShareJson(data) {
|
|
|
359
421
|
},
|
|
360
422
|
},
|
|
361
423
|
};
|
|
362
|
-
return
|
|
424
|
+
return { type: 'json', data: json };
|
|
363
425
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-icqq",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.14",
|
|
4
4
|
"description": "Zhin.js ICQQ adapter — direct connection to @icqqjs/icqq",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -34,21 +34,21 @@
|
|
|
34
34
|
"directory": "plugins/adapters/icqq"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@zhin.js/adapter": "1.1.
|
|
38
|
-
"@zhin.js/core": "1.5.
|
|
39
|
-
"@zhin.js/im-contract": "1.0.
|
|
37
|
+
"@zhin.js/adapter": "1.1.11",
|
|
38
|
+
"@zhin.js/core": "1.5.12",
|
|
39
|
+
"@zhin.js/im-contract": "1.0.4",
|
|
40
40
|
"@zhin.js/logger": "1.0.76",
|
|
41
|
-
"@zhin.js/permission": "1.0.
|
|
42
|
-
"@zhin.js/tool": "1.0.
|
|
41
|
+
"@zhin.js/permission": "1.0.3",
|
|
42
|
+
"@zhin.js/tool": "1.0.12"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@icqqjs/icqq": "^1.12.2",
|
|
46
46
|
"zod": "^4.0.0",
|
|
47
|
-
"@zhin.js/adapter": "1.1.
|
|
48
|
-
"@zhin.js/agent": "1.1.
|
|
49
|
-
"@zhin.js/command": "1.0.
|
|
50
|
-
"@zhin.js/core": "1.5.
|
|
51
|
-
"zhin.js": "6.0.
|
|
47
|
+
"@zhin.js/adapter": "1.1.11",
|
|
48
|
+
"@zhin.js/agent": "1.1.14",
|
|
49
|
+
"@zhin.js/command": "1.0.15",
|
|
50
|
+
"@zhin.js/core": "1.5.12",
|
|
51
|
+
"zhin.js": "6.0.12"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@icqqjs/icqq": {
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"typescript": "^6.0.3",
|
|
74
74
|
"vitest": "^4.1.10",
|
|
75
75
|
"zod": "^4.4.3",
|
|
76
|
-
"@zhin.js/agent": "1.1.
|
|
77
|
-
"zhin.js": "6.0.
|
|
76
|
+
"@zhin.js/agent": "1.1.14",
|
|
77
|
+
"zhin.js": "6.0.12"
|
|
78
78
|
},
|
|
79
79
|
"files": [
|
|
80
80
|
"adapters",
|
package/src/cq-message.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CQ 码与 MessageSegment 互转。
|
|
3
|
-
* 出站:@icqqjs/cli 要求 message 为非空字符串;引用段编码为 [reply:id](需 cli parse-message 支持 reply,见 scripts/patch-icqq-cli-reply.mjs)。
|
|
4
|
-
* 媒体段双向均为 canonical MediaRef(`data.media`),无 legacy url/file/base64 字段。
|
|
5
|
-
*/
|
|
1
|
+
/** ICQQ 历史文本视图 → canonical MessageSegment。出站只使用原生 Sendable。 */
|
|
6
2
|
import { isMediaRef, type MediaRef } from "@zhin.js/core";
|
|
7
|
-
import {
|
|
8
|
-
import { MessageSegment, segment, SendContent } from "zhin.js";
|
|
9
|
-
|
|
10
|
-
const logger = getLogger("icqq");
|
|
3
|
+
import type { MessageSegment } from "zhin.js";
|
|
11
4
|
|
|
12
5
|
const MAX_CQ_PARSE_LEN = 256_000;
|
|
13
6
|
|
|
@@ -29,23 +22,14 @@ export function icqqMediaRefFromString(value: string): MediaRef {
|
|
|
29
22
|
}
|
|
30
23
|
|
|
31
24
|
/**
|
|
32
|
-
* canonical MediaRef(`data.media`,唯一媒体来源)→
|
|
33
|
-
* base64
|
|
34
|
-
* 无 canonical 媒体引用时 warn 并返回 undefined(调用方丢弃该段)。
|
|
25
|
+
* canonical MediaRef(`data.media`,唯一媒体来源)→ ICQQ native
|
|
26
|
+
* element 的 file 参数。base64 使用 ICQQ 原生支持的 `base64://` 前缀。
|
|
35
27
|
*/
|
|
36
|
-
export function
|
|
37
|
-
type: string,
|
|
28
|
+
export function resolveIcqqMediaFile(
|
|
38
29
|
data: Record<string, unknown> | undefined,
|
|
39
30
|
): string | undefined {
|
|
40
31
|
const media = data?.media;
|
|
41
|
-
if (!isMediaRef(media))
|
|
42
|
-
logger.warn(formatCompact({
|
|
43
|
-
op: "icqq_outbound_media_dropped",
|
|
44
|
-
type,
|
|
45
|
-
reason: "missing_media_ref",
|
|
46
|
-
}));
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
32
|
+
if (!isMediaRef(media)) return undefined;
|
|
49
33
|
if (media.kind === "base64") {
|
|
50
34
|
return media.value.startsWith("base64://") ? media.value : `base64://${media.value}`;
|
|
51
35
|
}
|
|
@@ -75,7 +59,7 @@ function pushCqSegment(segments: MessageSegment[], type: string, arg: string): v
|
|
|
75
59
|
break;
|
|
76
60
|
case "record":
|
|
77
61
|
case "audio":
|
|
78
|
-
segments.push({ type: "
|
|
62
|
+
segments.push({ type: "audio", data: { media: icqqMediaRefFromString(arg) } });
|
|
79
63
|
break;
|
|
80
64
|
case "video":
|
|
81
65
|
segments.push({ type: "video", data: { media: icqqMediaRefFromString(arg) } });
|
|
@@ -127,54 +111,3 @@ export function parseCqMessage(raw: string): MessageSegment[] {
|
|
|
127
111
|
|
|
128
112
|
return segments.length ? segments : [{ type: "text", data: { text: raw } }];
|
|
129
113
|
}
|
|
130
|
-
|
|
131
|
-
/** 构建 icqq send_*_msg 的 message 字符串(非空) */
|
|
132
|
-
export function buildIcqqMessage(content: SendContent): string {
|
|
133
|
-
let message = toCqString(content).trim();
|
|
134
|
-
if (!message) message = "\u200b";
|
|
135
|
-
return message;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function toCqString(content: SendContent): string {
|
|
139
|
-
if (!Array.isArray(content)) content = [content];
|
|
140
|
-
return content
|
|
141
|
-
.map((seg) => {
|
|
142
|
-
if (typeof seg === "string") return seg;
|
|
143
|
-
const { type, data } = seg as MessageSegment;
|
|
144
|
-
switch (type) {
|
|
145
|
-
case "text":
|
|
146
|
-
return data.text ?? "";
|
|
147
|
-
case "face":
|
|
148
|
-
return `[face:${data.id}]`;
|
|
149
|
-
case "image": {
|
|
150
|
-
const arg = resolveCqMediaArg("image", data);
|
|
151
|
-
return arg ? `[image:${arg}]` : "";
|
|
152
|
-
}
|
|
153
|
-
case "at":
|
|
154
|
-
case "mention": {
|
|
155
|
-
const target = data.target ?? data.qq ?? data.id ?? data.user_id;
|
|
156
|
-
return `[at:${target}]`;
|
|
157
|
-
}
|
|
158
|
-
case "dice":
|
|
159
|
-
return "[dice]";
|
|
160
|
-
case "rps":
|
|
161
|
-
return "[rps]";
|
|
162
|
-
case "record":
|
|
163
|
-
case "audio": {
|
|
164
|
-
const arg = resolveCqMediaArg("record", data);
|
|
165
|
-
return arg ? `[record:${arg}]` : "";
|
|
166
|
-
}
|
|
167
|
-
case "video": {
|
|
168
|
-
const arg = resolveCqMediaArg("video", data);
|
|
169
|
-
return arg ? `[video:${arg}]` : "";
|
|
170
|
-
}
|
|
171
|
-
case "reply":
|
|
172
|
-
return `[reply:${data.message_id ?? data.id}]`;
|
|
173
|
-
case "json":
|
|
174
|
-
return `[json:${data.text ?? ''}]`;
|
|
175
|
-
default:
|
|
176
|
-
return segment.toString(seg);
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
.join("");
|
|
180
|
-
}
|