@zhin.js/adapter-icqq 1.1.10 → 1.1.12
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 +49 -0
- package/README.md +23 -12
- package/adapters/icqq/index.js +8 -0
- package/adapters/icqq/index.ts +8 -0
- package/lib/endpoint.js +30 -19
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +7 -0
- package/lib/protocol.js +47 -9
- package/package.json +16 -16
- package/src/endpoint.ts +30 -20
- package/src/index.ts +1 -0
- package/src/protocol.ts +47 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# @zhin.js/adapter-icqq
|
|
2
2
|
|
|
3
|
+
## 1.1.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a3ab153: Adapters can declare `segments.model` per segment type (extra optional string fields the model may fill, plus a prompt hint) that only that platform's Agent replies see. icqq now declares the segment types its encoder supports and a share extension, so the Agent can send music share cards from tool results.
|
|
8
|
+
- fe6555c: Messages mixing text with standalone ICQQ elements (share, file, voice, video, JSON card, forward) are now sent as ordered separate messages instead of failing with "must be sent as a standalone message". A failure after the first part is reported as `partial_delivery` with an unknown outcome so it is never resent.
|
|
9
|
+
- fbfefb7: Share segments accept `data.platform` (the song's music service: qq, netease, kugou, kuwo, migu). icqq maps it to the matching music app id when `config.appid` is absent and offers it to the Agent; adapters that cannot render share app metadata reject it like `audio` and `config`.
|
|
10
|
+
- Updated dependencies [1a11207]
|
|
11
|
+
- Updated dependencies [9943df6]
|
|
12
|
+
- Updated dependencies [5be09ae]
|
|
13
|
+
- Updated dependencies [a3ab153]
|
|
14
|
+
- Updated dependencies [e8f7309]
|
|
15
|
+
- Updated dependencies [5afc18f]
|
|
16
|
+
- Updated dependencies [d3563d9]
|
|
17
|
+
- Updated dependencies [c618eb7]
|
|
18
|
+
- Updated dependencies [fbfefb7]
|
|
19
|
+
- @zhin.js/tool@1.1.8
|
|
20
|
+
- @zhin.js/core@1.1.47
|
|
21
|
+
- @zhin.js/adapter@1.1.21
|
|
22
|
+
- @zhin.js/feature-kit@1.1.7
|
|
23
|
+
- @zhin.js/im-contract@1.1.5
|
|
24
|
+
- @zhin.js/agent-feature@1.1.8
|
|
25
|
+
- @zhin.js/skill@1.1.8
|
|
26
|
+
|
|
27
|
+
## 1.1.11
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- 9816c18: 将应用插件装配改为 configVersion: 2:主配置 plugins 映射实例名到包,disable_plugins 控制启停,业务配置使用顶层同名块。package.json 继续保留 features、entry、engine 等包契约,并支持静态 displayName/icon。
|
|
32
|
+
|
|
33
|
+
这是配置格式的破坏性更新:旧配置不能继续运行,请先停止 Bot,运行 `zhin migrate instances` 预览,再使用 `--apply` 转换;提供备份与中断继续/回滚,无法无损转换的嵌套、optional 和别名碰撞需人工迁移。Runtime、Console、CLI 与创建向导统一使用新声明。
|
|
34
|
+
|
|
35
|
+
保留框架顶层 `menuKeyword` / `menuStyle`,通过当前 generation 的 Root 配置视图供 Command Feature 消费;严格校验和配置修改不会丢失菜单设置,子实例配置保持隔离。
|
|
36
|
+
|
|
37
|
+
- Updated dependencies [c407bd2]
|
|
38
|
+
- Updated dependencies [064b06d]
|
|
39
|
+
- Updated dependencies [a50fc8a]
|
|
40
|
+
- Updated dependencies [b58548a]
|
|
41
|
+
- Updated dependencies [e914a93]
|
|
42
|
+
- Updated dependencies [e914a93]
|
|
43
|
+
- Updated dependencies [064b06d]
|
|
44
|
+
- @zhin.js/core@1.1.46
|
|
45
|
+
- @zhin.js/feature-kit@1.1.6
|
|
46
|
+
- @zhin.js/permission@1.1.7
|
|
47
|
+
- @zhin.js/adapter@1.1.20
|
|
48
|
+
- @zhin.js/agent-feature@1.1.7
|
|
49
|
+
- @zhin.js/skill@1.1.7
|
|
50
|
+
- @zhin.js/tool@1.1.7
|
|
51
|
+
|
|
3
52
|
## 1.1.10
|
|
4
53
|
|
|
5
54
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -41,25 +41,29 @@ pnpm add @icqqjs/qqsign
|
|
|
41
41
|
## 配置(Plugin Runtime)
|
|
42
42
|
|
|
43
43
|
```yaml
|
|
44
|
+
configVersion: 2
|
|
44
45
|
plugins:
|
|
45
|
-
icqq:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
icqq: "@zhin.js/adapter-icqq"
|
|
47
|
+
icqq:
|
|
48
|
+
master: "1659488338" # 必填,顶层共享(/approve 与 master 角色)
|
|
49
|
+
autoReconnect: true
|
|
50
|
+
endpoints:
|
|
51
|
+
- id: "${ICQQ_ACCOUNT}" # QQ 号
|
|
52
|
+
# password: "${ICQQ_PASSWORD}" # 可选;不填则扫码登录
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
多账号:一个插件实例挂多个 endpoint(`endpoints` 数组逐项覆盖顶层字段,`id` 必填):
|
|
54
56
|
|
|
55
57
|
```yaml
|
|
58
|
+
configVersion: 2
|
|
56
59
|
plugins:
|
|
57
|
-
icqq:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
icqq: "@zhin.js/adapter-icqq"
|
|
61
|
+
icqq:
|
|
62
|
+
master: "1659488338"
|
|
63
|
+
endpoints:
|
|
64
|
+
- id: "${ICQQ_ACCOUNT}"
|
|
65
|
+
- id: "${ICQQ_ACCOUNT_2}"
|
|
66
|
+
- id: "${ICQQ_ACCOUNT_3}"
|
|
63
67
|
```
|
|
64
68
|
|
|
65
69
|
`AdapterIndex` 会先合并插件实例默认值与 endpoint 覆盖值,再把一份完整配置交给
|
|
@@ -181,3 +185,10 @@ MIT
|
|
|
181
185
|
发送成功回执只接受平台真实消息 ID。网络超时、断线、缺少消息 ID 的响应会报告 `deliveryUnknown`,框架不会自动重发;手动重发可能重复发送。
|
|
182
186
|
|
|
183
187
|
ICQQ 原生分享和文件 API 可能已经成功,但只返回空值或文件 ID;这类结果不能当作可引用、可撤回的消息 ID,因此报告投递结果未知。平台操作仍只执行一次。文件撤回应使用平台专用文件 API。
|
|
188
|
+
|
|
189
|
+
### 独立元素按顺序拆分发送
|
|
190
|
+
|
|
191
|
+
ICQQ 要求分享、文件、语音、视频、JSON 卡片和合并转发单独成条。一条消息里同时有文字和这些元素时(例如 Agent 回复“找到了:”加一张音乐分享卡片),适配器按原顺序拆成多条依次发送,引用只附在第一条普通消息上。
|
|
192
|
+
|
|
193
|
+
- 回执的消息 ID 取第一条拿到 ID 的部分;所有部分都没有 ID(如单独一张分享)时,仍报告投递结果未知。
|
|
194
|
+
- 第一条就失败时原样报告失败;后面的部分失败时报告 `partial_delivery`(结果未知),因为前面的部分已经送达,重发会重复。
|
package/adapters/icqq/index.js
CHANGED
|
@@ -12,8 +12,16 @@ export default defineAdapter({
|
|
|
12
12
|
capabilities: ['inbound', 'outbound'],
|
|
13
13
|
operations: ['recall', 'reaction'],
|
|
14
14
|
segments: {
|
|
15
|
+
// Canonical types formatOutboundBody encodes; markdown / keyboard degrade to text first.
|
|
16
|
+
supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'forward', 'face', 'dice', 'rps', 'share'],
|
|
15
17
|
outboundMedia: ['base64', 'url', 'path'],
|
|
16
18
|
interactive: 'text',
|
|
19
|
+
model: {
|
|
20
|
+
share: {
|
|
21
|
+
fields: ['description', 'image', 'audio', 'platform'],
|
|
22
|
+
hint: 'renders as a QQ link card. For a song, set audio to the playable link, image to the cover, description to the artist and platform to its music service (qq, netease, kugou, kuwo or migu), copied from tool results; leave a field "" when you have no real value.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
17
25
|
},
|
|
18
26
|
create(context) {
|
|
19
27
|
const config = resolveIcqqConfig(context.config);
|
package/adapters/icqq/index.ts
CHANGED
|
@@ -23,8 +23,16 @@ export default defineAdapter<IcqqEndpointConfig>({
|
|
|
23
23
|
capabilities: ['inbound', 'outbound'],
|
|
24
24
|
operations: ['recall', 'reaction'],
|
|
25
25
|
segments: {
|
|
26
|
+
// Canonical types formatOutboundBody encodes; markdown / keyboard degrade to text first.
|
|
27
|
+
supported: ['text', 'mention', 'image', 'audio', 'video', 'file', 'reply', 'forward', 'face', 'dice', 'rps', 'share'],
|
|
26
28
|
outboundMedia: ['base64', 'url', 'path'],
|
|
27
29
|
interactive: 'text',
|
|
30
|
+
model: {
|
|
31
|
+
share: {
|
|
32
|
+
fields: ['description', 'image', 'audio', 'platform'],
|
|
33
|
+
hint: 'renders as a QQ link card. For a song, set audio to the playable link, image to the cover, description to the artist and platform to its music service (qq, netease, kugou, kuwo or migu), copied from tool results; leave a field "" when you have no real value.',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
28
36
|
},
|
|
29
37
|
create(context) {
|
|
30
38
|
const config = resolveIcqqConfig(context.config);
|
package/lib/endpoint.js
CHANGED
|
@@ -11,7 +11,7 @@ import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned,
|
|
|
11
11
|
import { isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
|
|
12
12
|
import { IcqqGuildCatalog, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
|
|
13
13
|
import { prepareIcqqOutboundMedia, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
|
|
14
|
-
import { formatInboundContent,
|
|
14
|
+
import { formatInboundContent, formatOutboundMessages, normalizeIcqqImageMedia, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
|
|
15
15
|
import { ManagedIcqqClient } from './managed-client.js';
|
|
16
16
|
const BOUND_EVENTS = [
|
|
17
17
|
'message.private',
|
|
@@ -292,28 +292,39 @@ export class IcqqEndpoint extends Endpoint {
|
|
|
292
292
|
? prepareIcqqOutboundMedia(normalizeIcqqImageMedia(payload), mediaMode)
|
|
293
293
|
: { content: payload, dispose() { } };
|
|
294
294
|
try {
|
|
295
|
-
|
|
296
|
-
const
|
|
295
|
+
// Standalone elements (share / file / …) are split into ordered messages.
|
|
296
|
+
const messages = formatOutboundMessages(prepared.content);
|
|
297
297
|
const parsed = icqqOutboundTarget(conversation);
|
|
298
|
-
let
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
298
|
+
let messageId;
|
|
299
|
+
for (const [index, message] of messages.entries()) {
|
|
300
|
+
const preview = formatIcqqSendablePreview(message);
|
|
301
|
+
let result;
|
|
302
|
+
try {
|
|
303
|
+
result = await this.#sendNative(parsed, message);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
this.#logger.warn(formatCompact({
|
|
307
|
+
op: 'icqq_send_failed',
|
|
308
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
309
|
+
part: messages.length > 1 ? `${index + 1}/${messages.length}` : undefined,
|
|
310
|
+
error: describeUnknownError(error),
|
|
311
|
+
preview: truncatePreview(preview, 120),
|
|
312
|
+
}));
|
|
313
|
+
if (index === 0)
|
|
314
|
+
throw error;
|
|
315
|
+
// Earlier parts are already delivered: a retry would duplicate them.
|
|
316
|
+
throw new EndpointDeliveryError('partial_delivery', `ICQQ delivered ${index} of ${messages.length} message parts`, 'unknown', { cause: error });
|
|
317
|
+
}
|
|
318
|
+
const rawId = result?.message_id;
|
|
319
|
+
const id = (typeof rawId === 'string' || (typeof rawId === 'number' && Number.isFinite(rawId))) && String(rawId).trim() !== ''
|
|
320
|
+
? String(rawId) : undefined;
|
|
321
|
+
messageId ??= id;
|
|
322
|
+
this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${id ?? '-'} | ${truncatePreview(preview, 80)}`);
|
|
310
323
|
}
|
|
311
|
-
|
|
312
|
-
if (
|
|
324
|
+
// Native share / file APIs return no id; the reply is confirmed by any part that has one.
|
|
325
|
+
if (!messageId) {
|
|
313
326
|
throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: ICQQ returned no message_id', 'unknown');
|
|
314
327
|
}
|
|
315
|
-
const messageId = String(rawId);
|
|
316
|
-
this.#logger.info(`send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(preview, 80)}`);
|
|
317
328
|
return messageId;
|
|
318
329
|
}
|
|
319
330
|
finally {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, type ActionResult, type IcqqEndpointConfig, type IcqqInboundMessage, type IcqqWireSegment, type ParsedIcqqSendTarget, type ResolvedIcqqConfig, } from './protocol.js';
|
|
1
|
+
export { Actions, formatInboundContent, formatOutboundBody, formatOutboundMessages, icqqInboundConversation, icqqOutboundTarget, type ActionResult, type IcqqEndpointConfig, type IcqqInboundMessage, type IcqqWireSegment, type ParsedIcqqSendTarget, type ResolvedIcqqConfig, } from './protocol.js';
|
|
2
2
|
export { IcqqEndpoint, type IcqqEndpointOptions, } from './endpoint.js';
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export { icqqClient, type IcqqClient, type IcqqClientEventMap, } from './client.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Actions, formatInboundContent, formatOutboundBody, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
|
|
1
|
+
export { Actions, formatInboundContent, formatOutboundBody, formatOutboundMessages, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
|
|
2
2
|
export { IcqqEndpoint, } from './endpoint.js';
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export { icqqClient, } from './client.js';
|
package/lib/protocol.d.ts
CHANGED
|
@@ -259,4 +259,11 @@ export declare function normalizeIcqqImageMedia(content: SendContent): SendConte
|
|
|
259
259
|
* notation; passing that notation would send it as literal text.
|
|
260
260
|
*/
|
|
261
261
|
export declare function formatOutboundBody(payload: unknown): Sendable;
|
|
262
|
+
/**
|
|
263
|
+
* Like formatOutboundBody, but a payload mixing standalone elements (share,
|
|
264
|
+
* file, record, video, json, long_msg) with other content becomes several
|
|
265
|
+
* messages in the original order instead of an error. Groups left with only a
|
|
266
|
+
* quote or whitespace are dropped: a standalone element cannot carry the quote.
|
|
267
|
+
*/
|
|
268
|
+
export declare function formatOutboundMessages(payload: unknown): Sendable[];
|
|
262
269
|
export declare function formatInboundContent(rawMessage: string): string;
|
package/lib/protocol.js
CHANGED
|
@@ -308,7 +308,44 @@ export function formatOutboundBody(payload) {
|
|
|
308
308
|
const text = String(payload).trim();
|
|
309
309
|
return text || '\u200b';
|
|
310
310
|
}
|
|
311
|
-
const elements = payload
|
|
311
|
+
const elements = formatOutboundElements(payload);
|
|
312
|
+
if (elements.length === 0)
|
|
313
|
+
return '\u200b';
|
|
314
|
+
const standalone = elements.find(isStandaloneElement);
|
|
315
|
+
if (standalone && elements.length !== 1) {
|
|
316
|
+
throw new TypeError(`ICQQ ${standalone.type} element must be sent as a standalone message`);
|
|
317
|
+
}
|
|
318
|
+
return elements.length === 1 ? elements[0] : elements;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Like formatOutboundBody, but a payload mixing standalone elements (share,
|
|
322
|
+
* file, record, video, json, long_msg) with other content becomes several
|
|
323
|
+
* messages in the original order instead of an error. Groups left with only a
|
|
324
|
+
* quote or whitespace are dropped: a standalone element cannot carry the quote.
|
|
325
|
+
*/
|
|
326
|
+
export function formatOutboundMessages(payload) {
|
|
327
|
+
if (!Array.isArray(payload)) {
|
|
328
|
+
return isWireSegment(payload) ? formatOutboundMessages([payload]) : [formatOutboundBody(payload)];
|
|
329
|
+
}
|
|
330
|
+
const groups = [[]];
|
|
331
|
+
for (const element of formatOutboundElements(payload)) {
|
|
332
|
+
if (isStandaloneElement(element))
|
|
333
|
+
groups.push([element], []);
|
|
334
|
+
else
|
|
335
|
+
groups.at(-1).push(element);
|
|
336
|
+
}
|
|
337
|
+
const messages = groups.filter(group => group.some(element => typeof element === 'string'
|
|
338
|
+
? element.trim() !== ''
|
|
339
|
+
: element.type !== 'reply'));
|
|
340
|
+
if (messages.length === 0)
|
|
341
|
+
return ['\u200b'];
|
|
342
|
+
return messages.map(group => group.length === 1 ? group[0] : group);
|
|
343
|
+
}
|
|
344
|
+
function isStandaloneElement(element) {
|
|
345
|
+
return typeof element !== 'string' && ICQQ_STANDALONE_ELEMENTS.has(element.type);
|
|
346
|
+
}
|
|
347
|
+
function formatOutboundElements(payload) {
|
|
348
|
+
return payload.flatMap((seg) => {
|
|
312
349
|
if (typeof seg === 'string')
|
|
313
350
|
return seg;
|
|
314
351
|
const item = seg;
|
|
@@ -385,13 +422,6 @@ export function formatOutboundBody(payload) {
|
|
|
385
422
|
}
|
|
386
423
|
}
|
|
387
424
|
}).filter((element) => typeof element !== 'string' || element.length > 0);
|
|
388
|
-
if (elements.length === 0)
|
|
389
|
-
return '\u200b';
|
|
390
|
-
const standalone = elements.find((element) => typeof element !== 'string' && ICQQ_STANDALONE_ELEMENTS.has(element.type));
|
|
391
|
-
if (standalone && elements.length !== 1) {
|
|
392
|
-
throw new TypeError(`ICQQ ${standalone.type} element must be sent as a standalone message`);
|
|
393
|
-
}
|
|
394
|
-
return elements.length === 1 ? elements[0] : elements;
|
|
395
425
|
}
|
|
396
426
|
export function formatInboundContent(rawMessage) {
|
|
397
427
|
return rawMessage;
|
|
@@ -402,6 +432,14 @@ function isWireSegment(value) {
|
|
|
402
432
|
&& value !== null
|
|
403
433
|
&& typeof value.type === 'string';
|
|
404
434
|
}
|
|
435
|
+
/** Music service → ICQQ share app; ICQQ fills appname / appsign from its own table for these ids. */
|
|
436
|
+
const ICQQ_MUSIC_APPIDS = {
|
|
437
|
+
qq: 100497308,
|
|
438
|
+
netease: 100495085,
|
|
439
|
+
kugou: 205141,
|
|
440
|
+
kuwo: 100243533,
|
|
441
|
+
migu: 1101053067,
|
|
442
|
+
};
|
|
405
443
|
function formatShare(data) {
|
|
406
444
|
if (!data)
|
|
407
445
|
throw new TypeError('ICQQ share segment requires data');
|
|
@@ -416,7 +454,7 @@ function formatShare(data) {
|
|
|
416
454
|
const text = String(value ?? '').trim();
|
|
417
455
|
return text || undefined;
|
|
418
456
|
};
|
|
419
|
-
const appid = Number(config?.appid);
|
|
457
|
+
const appid = Number(config?.appid ?? ICQQ_MUSIC_APPIDS[String(data.platform ?? '').trim().toLowerCase()]);
|
|
420
458
|
const appname = optionalString(config?.package);
|
|
421
459
|
const appsign = optionalString(config?.sign);
|
|
422
460
|
const shareConfig = Number.isSafeInteger(appid) && appid > 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-icqq",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.12",
|
|
4
4
|
"description": "Zhin.js ICQQ adapter — direct connection to @icqqjs/icqq",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -33,24 +33,24 @@
|
|
|
33
33
|
"directory": "plugins/adapters/icqq"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@zhin.js/adapter": "1.1.
|
|
37
|
-
"@zhin.js/
|
|
38
|
-
"@zhin.js/
|
|
39
|
-
"@zhin.js/
|
|
40
|
-
"@zhin.js/
|
|
36
|
+
"@zhin.js/adapter": "1.1.21",
|
|
37
|
+
"@zhin.js/core": "1.1.47",
|
|
38
|
+
"@zhin.js/feature-kit": "1.1.7",
|
|
39
|
+
"@zhin.js/im-contract": "1.1.5",
|
|
40
|
+
"@zhin.js/agent-feature": "1.1.8",
|
|
41
41
|
"@zhin.js/logger": "1.1.2",
|
|
42
|
-
"@zhin.js/permission": "1.1.
|
|
43
|
-
"@zhin.js/skill": "1.1.
|
|
44
|
-
"@zhin.js/tool": "1.1.
|
|
42
|
+
"@zhin.js/permission": "1.1.7",
|
|
43
|
+
"@zhin.js/skill": "1.1.8",
|
|
44
|
+
"@zhin.js/tool": "1.1.8"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@icqqjs/icqq": "^1.12.2",
|
|
48
48
|
"zod": "^4.0.0",
|
|
49
|
-
"@zhin.js/adapter": "^1.1.
|
|
50
|
-
"@zhin.js/command": "^1.1.
|
|
51
|
-
"@zhin.js/core": "^1.1.
|
|
52
|
-
"@zhin.js/prompt-section": "^1.1.
|
|
53
|
-
"zhin.js": "^1.1.
|
|
49
|
+
"@zhin.js/adapter": "^1.1.21",
|
|
50
|
+
"@zhin.js/command": "^1.1.9",
|
|
51
|
+
"@zhin.js/core": "^1.1.47",
|
|
52
|
+
"@zhin.js/prompt-section": "^1.1.7",
|
|
53
|
+
"zhin.js": "^1.1.12"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"@icqqjs/icqq": {
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
"typescript": "^6.0.3",
|
|
76
76
|
"vitest": "^4.1.10",
|
|
77
77
|
"zod": "^4.4.3",
|
|
78
|
-
"@zhin.js/prompt-section": "1.1.
|
|
79
|
-
"zhin.js": "1.1.
|
|
78
|
+
"@zhin.js/prompt-section": "1.1.7",
|
|
79
|
+
"zhin.js": "1.1.12"
|
|
80
80
|
},
|
|
81
81
|
"files": [
|
|
82
82
|
"adapters",
|
package/src/endpoint.ts
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
} from './outbound-media.js';
|
|
53
53
|
import {
|
|
54
54
|
formatInboundContent,
|
|
55
|
-
|
|
55
|
+
formatOutboundMessages,
|
|
56
56
|
normalizeIcqqImageMedia,
|
|
57
57
|
icqqInboundConversation,
|
|
58
58
|
icqqOutboundTarget,
|
|
@@ -350,29 +350,39 @@ export class IcqqEndpoint extends Endpoint<Client> {
|
|
|
350
350
|
? prepareIcqqOutboundMedia(normalizeIcqqImageMedia(payload), mediaMode)
|
|
351
351
|
: { content: payload, dispose() {} };
|
|
352
352
|
try {
|
|
353
|
-
|
|
354
|
-
const
|
|
353
|
+
// Standalone elements (share / file / …) are split into ordered messages.
|
|
354
|
+
const messages = formatOutboundMessages(prepared.content);
|
|
355
355
|
const parsed = icqqOutboundTarget(conversation);
|
|
356
|
-
let
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
356
|
+
let messageId: string | undefined;
|
|
357
|
+
for (const [index, message] of messages.entries()) {
|
|
358
|
+
const preview = formatIcqqSendablePreview(message);
|
|
359
|
+
let result: { message_id?: unknown };
|
|
360
|
+
try {
|
|
361
|
+
result = await this.#sendNative(parsed, message);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
this.#logger.warn(formatCompact({
|
|
364
|
+
op: 'icqq_send_failed',
|
|
365
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
366
|
+
part: messages.length > 1 ? `${index + 1}/${messages.length}` : undefined,
|
|
367
|
+
error: describeUnknownError(error),
|
|
368
|
+
preview: truncatePreview(preview, 120),
|
|
369
|
+
}));
|
|
370
|
+
if (index === 0) throw error;
|
|
371
|
+
// Earlier parts are already delivered: a retry would duplicate them.
|
|
372
|
+
throw new EndpointDeliveryError('partial_delivery', `ICQQ delivered ${index} of ${messages.length} message parts`, 'unknown', { cause: error });
|
|
373
|
+
}
|
|
374
|
+
const rawId = result?.message_id;
|
|
375
|
+
const id = (typeof rawId === 'string' || (typeof rawId === 'number' && Number.isFinite(rawId))) && String(rawId).trim() !== ''
|
|
376
|
+
? String(rawId) : undefined;
|
|
377
|
+
messageId ??= id;
|
|
378
|
+
this.#logger.info(
|
|
379
|
+
`send ${conversation.kind}:${conversation.id} | id: ${id ?? '-'} | ${truncatePreview(preview, 80)}`,
|
|
380
|
+
);
|
|
367
381
|
}
|
|
368
|
-
|
|
369
|
-
if (
|
|
382
|
+
// Native share / file APIs return no id; the reply is confirmed by any part that has one.
|
|
383
|
+
if (!messageId) {
|
|
370
384
|
throw new EndpointDeliveryError('delivery_unconfirmed', 'Message delivery is unconfirmed: ICQQ returned no message_id', 'unknown');
|
|
371
385
|
}
|
|
372
|
-
const messageId = String(rawId);
|
|
373
|
-
this.#logger.info(
|
|
374
|
-
`send ${conversation.kind}:${conversation.id} | id: ${messageId} | ${truncatePreview(preview, 80)}`,
|
|
375
|
-
);
|
|
376
386
|
return messageId;
|
|
377
387
|
} finally {
|
|
378
388
|
prepared.dispose();
|
package/src/index.ts
CHANGED
package/src/protocol.ts
CHANGED
|
@@ -435,7 +435,43 @@ export function formatOutboundBody(payload: unknown): Sendable {
|
|
|
435
435
|
const text = String(payload).trim();
|
|
436
436
|
return text || '\u200b';
|
|
437
437
|
}
|
|
438
|
-
const elements = payload
|
|
438
|
+
const elements = formatOutboundElements(payload);
|
|
439
|
+
if (elements.length === 0) return '\u200b';
|
|
440
|
+
const standalone = elements.find(isStandaloneElement);
|
|
441
|
+
if (standalone && elements.length !== 1) {
|
|
442
|
+
throw new TypeError(`ICQQ ${standalone.type} element must be sent as a standalone message`);
|
|
443
|
+
}
|
|
444
|
+
return elements.length === 1 ? elements[0]! : elements;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Like formatOutboundBody, but a payload mixing standalone elements (share,
|
|
449
|
+
* file, record, video, json, long_msg) with other content becomes several
|
|
450
|
+
* messages in the original order instead of an error. Groups left with only a
|
|
451
|
+
* quote or whitespace are dropped: a standalone element cannot carry the quote.
|
|
452
|
+
*/
|
|
453
|
+
export function formatOutboundMessages(payload: unknown): Sendable[] {
|
|
454
|
+
if (!Array.isArray(payload)) {
|
|
455
|
+
return isWireSegment(payload) ? formatOutboundMessages([payload]) : [formatOutboundBody(payload)];
|
|
456
|
+
}
|
|
457
|
+
const groups: (string | MessageElem)[][] = [[]];
|
|
458
|
+
for (const element of formatOutboundElements(payload)) {
|
|
459
|
+
if (isStandaloneElement(element)) groups.push([element], []);
|
|
460
|
+
else groups.at(-1)!.push(element);
|
|
461
|
+
}
|
|
462
|
+
const messages = groups.filter(group => group.some(element => typeof element === 'string'
|
|
463
|
+
? element.trim() !== ''
|
|
464
|
+
: element.type !== 'reply'));
|
|
465
|
+
if (messages.length === 0) return ['\u200b'];
|
|
466
|
+
return messages.map(group => group.length === 1 ? group[0]! : group);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function isStandaloneElement(element: string | MessageElem): element is MessageElem {
|
|
470
|
+
return typeof element !== 'string' && ICQQ_STANDALONE_ELEMENTS.has(element.type as never);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function formatOutboundElements(payload: readonly unknown[]): (string | MessageElem)[] {
|
|
474
|
+
return payload.flatMap<string | MessageElem>((seg) => {
|
|
439
475
|
if (typeof seg === 'string') return seg;
|
|
440
476
|
const item = seg as IcqqWireSegment;
|
|
441
477
|
switch (item.type) {
|
|
@@ -503,16 +539,6 @@ export function formatOutboundBody(payload: unknown): Sendable {
|
|
|
503
539
|
}
|
|
504
540
|
}
|
|
505
541
|
}).filter((element) => typeof element !== 'string' || element.length > 0);
|
|
506
|
-
|
|
507
|
-
if (elements.length === 0) return '\u200b';
|
|
508
|
-
const standalone = elements.find(
|
|
509
|
-
(element): element is MessageElem =>
|
|
510
|
-
typeof element !== 'string' && ICQQ_STANDALONE_ELEMENTS.has(element.type as never),
|
|
511
|
-
);
|
|
512
|
-
if (standalone && elements.length !== 1) {
|
|
513
|
-
throw new TypeError(`ICQQ ${standalone.type} element must be sent as a standalone message`);
|
|
514
|
-
}
|
|
515
|
-
return elements.length === 1 ? elements[0]! : elements;
|
|
516
542
|
}
|
|
517
543
|
|
|
518
544
|
export function formatInboundContent(rawMessage: string): string {
|
|
@@ -526,6 +552,15 @@ function isWireSegment(value: unknown): value is IcqqWireSegment {
|
|
|
526
552
|
&& typeof (value as { type?: unknown }).type === 'string';
|
|
527
553
|
}
|
|
528
554
|
|
|
555
|
+
/** Music service → ICQQ share app; ICQQ fills appname / appsign from its own table for these ids. */
|
|
556
|
+
const ICQQ_MUSIC_APPIDS: Readonly<Record<string, number>> = {
|
|
557
|
+
qq: 100497308,
|
|
558
|
+
netease: 100495085,
|
|
559
|
+
kugou: 205141,
|
|
560
|
+
kuwo: 100243533,
|
|
561
|
+
migu: 1101053067,
|
|
562
|
+
};
|
|
563
|
+
|
|
529
564
|
function formatShare(data: Record<string, unknown> | undefined): ShareElem {
|
|
530
565
|
if (!data) throw new TypeError('ICQQ share segment requires data');
|
|
531
566
|
const config = data.config as {
|
|
@@ -542,7 +577,7 @@ function formatShare(data: Record<string, unknown> | undefined): ShareElem {
|
|
|
542
577
|
const text = String(value ?? '').trim();
|
|
543
578
|
return text || undefined;
|
|
544
579
|
};
|
|
545
|
-
const appid = Number(config?.appid);
|
|
580
|
+
const appid = Number(config?.appid ?? ICQQ_MUSIC_APPIDS[String(data.platform ?? '').trim().toLowerCase()]);
|
|
546
581
|
const appname = optionalString(config?.package);
|
|
547
582
|
const appsign = optionalString(config?.sign);
|
|
548
583
|
const shareConfig = Number.isSafeInteger(appid) && appid > 0
|