@zhin.js/adapter-icqq 1.1.11 → 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 CHANGED
@@ -1,5 +1,29 @@
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
+
3
27
  ## 1.1.11
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -185,3 +185,10 @@ MIT
185
185
  发送成功回执只接受平台真实消息 ID。网络超时、断线、缺少消息 ID 的响应会报告 `deliveryUnknown`,框架不会自动重发;手动重发可能重复发送。
186
186
 
187
187
  ICQQ 原生分享和文件 API 可能已经成功,但只返回空值或文件 ID;这类结果不能当作可引用、可撤回的消息 ID,因此报告投递结果未知。平台操作仍只执行一次。文件撤回应使用平台专用文件 API。
188
+
189
+ ### 独立元素按顺序拆分发送
190
+
191
+ ICQQ 要求分享、文件、语音、视频、JSON 卡片和合并转发单独成条。一条消息里同时有文字和这些元素时(例如 Agent 回复“找到了:”加一张音乐分享卡片),适配器按原顺序拆成多条依次发送,引用只附在第一条普通消息上。
192
+
193
+ - 回执的消息 ID 取第一条拿到 ID 的部分;所有部分都没有 ID(如单独一张分享)时,仍报告投递结果未知。
194
+ - 第一条就失败时原样报告失败;后面的部分失败时报告 `partial_delivery`(结果未知),因为前面的部分已经送达,重发会重复。
@@ -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);
@@ -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, formatOutboundBody, normalizeIcqqImageMedia, icqqInboundConversation, icqqOutboundTarget, } from './protocol.js';
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
- const message = formatOutboundBody(prepared.content);
296
- const preview = formatIcqqSendablePreview(message);
295
+ // Standalone elements (share / file / …) are split into ordered messages.
296
+ const messages = formatOutboundMessages(prepared.content);
297
297
  const parsed = icqqOutboundTarget(conversation);
298
- let result;
299
- try {
300
- result = await this.#sendNative(parsed, message);
301
- }
302
- catch (error) {
303
- this.#logger.warn(formatCompact({
304
- op: 'icqq_send_failed',
305
- target: `${conversation.kind}:${conversation.id}`,
306
- error: describeUnknownError(error),
307
- preview: truncatePreview(preview, 120),
308
- }));
309
- throw error;
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
- const rawId = result?.message_id;
312
- if ((typeof rawId !== 'string' && typeof rawId !== 'number') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
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.flatMap((seg) => {
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.11",
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.20",
37
- "@zhin.js/agent-feature": "1.1.7",
38
- "@zhin.js/core": "1.1.46",
39
- "@zhin.js/feature-kit": "1.1.6",
40
- "@zhin.js/im-contract": "1.1.4",
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
42
  "@zhin.js/permission": "1.1.7",
43
- "@zhin.js/skill": "1.1.7",
44
- "@zhin.js/tool": "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.20",
50
- "@zhin.js/command": "^1.1.8",
51
- "@zhin.js/core": "^1.1.46",
52
- "@zhin.js/prompt-section": "^1.1.6",
53
- "zhin.js": "^1.1.11"
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.6",
79
- "zhin.js": "1.1.11"
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
- formatOutboundBody,
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
- const message = formatOutboundBody(prepared.content);
354
- const preview = formatIcqqSendablePreview(message);
353
+ // Standalone elements (share / file / …) are split into ordered messages.
354
+ const messages = formatOutboundMessages(prepared.content);
355
355
  const parsed = icqqOutboundTarget(conversation);
356
- let result: { message_id?: unknown };
357
- try {
358
- result = await this.#sendNative(parsed, message);
359
- } catch (error) {
360
- this.#logger.warn(formatCompact({
361
- op: 'icqq_send_failed',
362
- target: `${conversation.kind}:${conversation.id}`,
363
- error: describeUnknownError(error),
364
- preview: truncatePreview(preview, 120),
365
- }));
366
- throw error;
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
- const rawId = result?.message_id;
369
- if ((typeof rawId !== 'string' && typeof rawId !== 'number') || String(rawId).trim() === '' || (typeof rawId === 'number' && !Number.isFinite(rawId))) {
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
@@ -2,6 +2,7 @@ export {
2
2
  Actions,
3
3
  formatInboundContent,
4
4
  formatOutboundBody,
5
+ formatOutboundMessages,
5
6
  icqqInboundConversation,
6
7
  icqqOutboundTarget,
7
8
  type ActionResult,
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.flatMap<string | MessageElem>((seg) => {
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