@zhin.js/adapter-icqq 7.0.0 → 7.0.2

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,55 @@
1
1
  # @zhin.js/adapter-icqq
2
2
 
3
+ ## 7.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d5cd4aa: Publish Plugin Runtime entry points and convention modules as JavaScript so
8
+ installed npm plugins load on Node without TypeScript stripping. Workspace
9
+ development continues to prefer TypeScript sources for local HMR.
10
+
11
+ Remove the unconsumed legacy game hub APIs from game-kit; game navigation and
12
+ records now use ordinary convention commands owned by the game hub plugin.
13
+
14
+ - Updated dependencies [d5cd4aa]
15
+ - @zhin.js/command@1.0.4
16
+ - zhin.js@5.0.2
17
+ - @zhin.js/adapter@1.1.2
18
+ - @zhin.js/core@1.4.2
19
+ - @zhin.js/agent@1.0.7
20
+
21
+ ## 7.0.1
22
+
23
+ ### Patch Changes
24
+
25
+ - 74b035c: endpoint 管理命令扩展至 18/20 适配器:kook / discord / github(private_key 支持内联文件路径)/ icqq(bindFlow 登记式 add + `icqq login` 引导)/ dingtalk / lark / line / satori / wechat-mp / wecom / weixin-ilink 接入 `endpoint list/add/remove`(字段对齐各自 schema,凭据写 `.env`)。email(smtp/imap 嵌套对象)与 sandbox(无凭据)暂不接。
26
+ - 6cb6152: 统一消息元素通道(UNI-Channel)落地:
27
+
28
+ - **入站契约**:`IncomingMessage.segments`(canonical Segment[],与 content 纯文本视图同源双轨),Message 透传;AI 兜底链路经 `collectSegmentMedia` 把图片/语音/视频/文件 MediaRef 写入会话 extra——多模态输入不再丢失。
29
+ - **出站协商**:`normalizeOutboundPayload` 升级全量 canonical 归一(复用 generic-segment-mapper),html→image 按端点 `segments.outboundMedia` 声明降级(base64 直发 / url-or-text / passthrough 自行物化);`MediaRef.kind` 新增 `'file'` 承载平台不透明引用(file_id/resource_id)。
30
+ - **能力声明**:`defineAdapter.segments` policy(outboundMedia / interactive),三道段门禁复活(探测点改 adapters/\*.ts,豁免名单渐进收敛)。
31
+ - **首批迁移**:icqq 全保真出入站(CQ ↔ canonical,quote→reply 段);milky/telegram/discord 入站媒体段恢复(附件/贴纸/callback action);napcat/onebot11/onebot12 出站 canonical→OneBot 数组段;wechat-mp/wecom `/cgi-bin/media/upload` 与 lark `/im/v1/images` 上传通路(base64/URL 图片不再静默丢图,失败降级文本)。
32
+
33
+ - Updated dependencies [cdf64e7]
34
+ - Updated dependencies [2d0a159]
35
+ - Updated dependencies [5691aba]
36
+ - Updated dependencies [078e3f7]
37
+ - Updated dependencies [50497a5]
38
+ - Updated dependencies [9c997b2]
39
+ - Updated dependencies [09d4f25]
40
+ - Updated dependencies [43485a9]
41
+ - Updated dependencies [f0ec5ab]
42
+ - Updated dependencies [3e925d0]
43
+ - Updated dependencies [fa66c4c]
44
+ - Updated dependencies [fa66c4c]
45
+ - Updated dependencies [6cb6152]
46
+ - @zhin.js/command@1.0.3
47
+ - @zhin.js/agent@1.0.6
48
+ - @zhin.js/plugin-runtime@1.1.1
49
+ - zhin.js@5.0.1
50
+ - @zhin.js/adapter@1.1.1
51
+ - @zhin.js/core@1.4.1
52
+
3
53
  ## 7.0.0
4
54
 
5
55
  ### Patch Changes
@@ -0,0 +1,89 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ /**
3
+ * Convention entry: discover `adapters/icqq.ts` → defineAdapter.
4
+ */
5
+ import { defineAdapter } from '@zhin.js/adapter';
6
+ import { messageGatewayToken } from '@zhin.js/core/runtime';
7
+ import { formatCompact, getLogger } from '@zhin.js/logger';
8
+ import { databaseHostToken, INBOX_TABLE_NOTICE, INBOX_TABLE_REQUEST, runtimeEventPublisherToken, } from '@zhin.js/plugin-runtime';
9
+ import { IcqqIpcEndpoint } from "../lib/endpoint.js";
10
+ import { icqqRuntimeStateToken } from "../lib/icqq-runtime-state.js";
11
+ import { resolveIcqqConfig, } from "../lib/protocol.js";
12
+ export { IcqqIpcEndpoint } from "../lib/endpoint.js";
13
+ const logger = getLogger('icqq');
14
+ export default defineAdapter({
15
+ capabilities: ['inbound', 'outbound'],
16
+ // 统一消息元素通道(UNI-Channel):端点可消费 base64 / url / upload 媒体
17
+ // (path 由出站物化按 file/base64 模式转换,非声明消费形态);
18
+ // 卡片/按钮等富交互段无原生承载,降级纯文本。
19
+ segments: {
20
+ outboundMedia: ['base64', 'url', 'upload'],
21
+ interactive: 'text',
22
+ },
23
+ create(context) {
24
+ // Client-library / IPC daemon path — no httpHostToken.
25
+ // Console loginAssist + host-router routes deferred.
26
+ const config = resolveIcqqConfig(context.config);
27
+ // 注册到插件运行时状态(icqq endpoint list 的"运行中"数据源)
28
+ context.use(icqqRuntimeStateToken).endpoints.set(config.name, {
29
+ name: config.name,
30
+ mode: config.rpc ? 'rpc' : 'ipc',
31
+ });
32
+ const inbox = resolveInboxHooks(context);
33
+ return new IcqqIpcEndpoint({
34
+ id: context.id,
35
+ gateway: context.use(messageGatewayToken),
36
+ config,
37
+ ...(inbox ? { inbox } : {}),
38
+ });
39
+ },
40
+ });
41
+ /** 资源可选(轻量 Root 无 DatabaseHost / console hub):缺失时降级为不写收件箱。 */
42
+ function tryUse(context, token) {
43
+ try {
44
+ return context.use(token);
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ /**
51
+ * 收件箱钩子:写 unified_inbox_request/notice(按 platform_*_id 去重,
52
+ * 推送事件与 GET_SYSTEM_MSG 首拉可能重复同一请求),并经 console hub 广播。
53
+ */
54
+ function resolveInboxHooks(context) {
55
+ const host = tryUse(context, databaseHostToken);
56
+ const publisher = tryUse(context, runtimeEventPublisherToken);
57
+ if (!host && !publisher)
58
+ return undefined;
59
+ const insertDeduped = async (table, idField, row) => {
60
+ const model = host?.models.get(table);
61
+ if (!model)
62
+ return;
63
+ try {
64
+ const existing = await model.select().where({
65
+ adapter: row.adapter,
66
+ endpoint_id: row.endpoint_id,
67
+ [idField]: row[idField],
68
+ });
69
+ if (Array.isArray(existing) && existing.length > 0)
70
+ return;
71
+ await model.insert(row);
72
+ }
73
+ catch (error) {
74
+ logger.warn(formatCompact({
75
+ op: 'inbox_insert',
76
+ table,
77
+ error: error instanceof Error ? error.message : String(error),
78
+ }));
79
+ }
80
+ };
81
+ const publish = publisher;
82
+ return {
83
+ recordRequest: (row) => insertDeduped(INBOX_TABLE_REQUEST, 'platform_request_id', row),
84
+ recordNotice: (row) => insertDeduped(INBOX_TABLE_NOTICE, 'platform_notice_id', row),
85
+ ...(publish
86
+ ? { publish: (type, data) => publish.publish(type, data) }
87
+ : {}),
88
+ };
89
+ }
package/adapters/icqq.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  type Token,
13
13
  } from '@zhin.js/plugin-runtime';
14
14
  import { IcqqIpcEndpoint, type IcqqInboxHooks } from '../src/endpoint.js';
15
+ import { icqqRuntimeStateToken } from '../src/icqq-runtime-state.js';
15
16
  import {
16
17
  resolveIcqqConfig,
17
18
  type IcqqAdapterConfig,
@@ -28,14 +29,27 @@ const logger = getLogger('icqq');
28
29
 
29
30
  export default defineAdapter<IcqqAdapterConfig>({
30
31
  capabilities: ['inbound', 'outbound'],
32
+ // 统一消息元素通道(UNI-Channel):端点可消费 base64 / url / upload 媒体
33
+ // (path 由出站物化按 file/base64 模式转换,非声明消费形态);
34
+ // 卡片/按钮等富交互段无原生承载,降级纯文本。
35
+ segments: {
36
+ outboundMedia: ['base64', 'url', 'upload'],
37
+ interactive: 'text',
38
+ },
31
39
  create(context) {
32
40
  // Client-library / IPC daemon path — no httpHostToken.
33
41
  // Console loginAssist + host-router routes deferred.
42
+ const config = resolveIcqqConfig(context.config);
43
+ // 注册到插件运行时状态(icqq endpoint list 的"运行中"数据源)
44
+ context.use(icqqRuntimeStateToken).endpoints.set(config.name, {
45
+ name: config.name,
46
+ mode: config.rpc ? 'rpc' : 'ipc',
47
+ });
34
48
  const inbox = resolveInboxHooks(context);
35
49
  return new IcqqIpcEndpoint({
36
50
  id: context.id,
37
51
  gateway: context.use(messageGatewayToken),
38
- config: resolveIcqqConfig(context.config),
52
+ config,
39
53
  ...(inbox ? { inbox } : {}),
40
54
  });
41
55
  },
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { icqqEndpointCommands } from "../../../lib/icqq-endpoint-commands.js";
3
+ export default icqqEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ import { icqqEndpointCommands } from '../../../src/icqq-endpoint-commands.js';
2
+
3
+ export default icqqEndpointCommands.add;
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { icqqEndpointCommands } from "../../lib/icqq-endpoint-commands.js";
3
+ export default icqqEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ import { icqqEndpointCommands } from '../../src/icqq-endpoint-commands.js';
2
+
3
+ export default icqqEndpointCommands.list;
@@ -0,0 +1,3 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { icqqEndpointCommands } from "../../../lib/icqq-endpoint-commands.js";
3
+ export default icqqEndpointCommands.remove;
@@ -0,0 +1,3 @@
1
+ import { icqqEndpointCommands } from '../../../src/icqq-endpoint-commands.js';
2
+
3
+ export default icqqEndpointCommands.remove;
package/lib/endpoint.js CHANGED
@@ -494,14 +494,38 @@ export class IcqqIpcEndpoint {
494
494
  }
495
495
  /** Test / internal: admit when open. */
496
496
  admit(msg) {
497
- if (!this.#open)
497
+ if (!this.#open) {
498
+ logger.debug(formatCompact({
499
+ op: 'icqq_inbound_skip',
500
+ reason: 'endpoint_closed',
501
+ endpoint: this.name,
502
+ id: msg.id,
503
+ target: msg.target,
504
+ }));
498
505
  return;
499
- // 新 Runtime Message.content 为纯文本:@ 本机(uin = name)只能经 metadata 传递
500
- const mentioned = isIcqqBotMentioned({ uin: this.name, rawMessage: msg.content });
506
+ }
507
+ // 新 Runtime Message.content 为纯文本:@ 本机(uin = name)经结构化段 / metadata 传递
508
+ const mentioned = isIcqqBotMentioned({
509
+ uin: this.name,
510
+ content: msg.segments,
511
+ rawMessage: msg.content,
512
+ });
513
+ logger.debug(formatCompact({
514
+ op: 'icqq_inbound_admit',
515
+ endpoint: this.name,
516
+ id: msg.id,
517
+ target: msg.target,
518
+ sender: msg.sender,
519
+ channelType: msg.channelType,
520
+ mentioned: mentioned || undefined,
521
+ preview: msg.content.slice(0, 80),
522
+ segments: summarizeSegmentTypes(msg.segments),
523
+ }));
501
524
  void this.#options.gateway.receive({
502
525
  adapter: this.#options.id,
503
526
  target: msg.target,
504
527
  content: msg.content,
528
+ ...(msg.segments ? { segments: msg.segments } : {}),
505
529
  sender: msg.sender,
506
530
  id: msg.id,
507
531
  metadata: Object.freeze({
@@ -555,13 +579,36 @@ export class IcqqIpcEndpoint {
555
579
  if (!isIcqqMessagePostType(payload))
556
580
  return;
557
581
  const data = payload;
558
- if (shouldSkipSelfInboundMessage(data))
582
+ if (shouldSkipSelfInboundMessage(data)) {
583
+ logger.debug(formatCompact({
584
+ op: 'icqq_inbound_skip',
585
+ reason: 'self',
586
+ endpoint: this.name,
587
+ user_id: data.user_id,
588
+ self_id: data.self_id,
589
+ }));
559
590
  return;
591
+ }
560
592
  const normalized = normalizeIcqqInboundMessage(data);
561
- if (!normalized)
593
+ if (!normalized) {
594
+ logger.debug(formatCompact({
595
+ op: 'icqq_inbound_skip',
596
+ reason: 'normalize_failed',
597
+ endpoint: this.name,
598
+ message_id: data.message_id,
599
+ message_type: data.message_type ?? data.type,
600
+ }));
562
601
  return;
563
- if (!this.#inboundDeduper.shouldProcess(normalized.messageId))
602
+ }
603
+ if (!this.#inboundDeduper.shouldProcess(normalized.messageId)) {
604
+ logger.debug(formatCompact({
605
+ op: 'icqq_inbound_skip',
606
+ reason: 'dedupe',
607
+ endpoint: this.name,
608
+ id: normalized.messageId,
609
+ }));
564
610
  return;
611
+ }
565
612
  this.admit({
566
613
  id: normalized.messageId,
567
614
  target: formatInboundTarget({
@@ -570,6 +617,7 @@ export class IcqqIpcEndpoint {
570
617
  channelParentGroupId: normalized.channelParentGroupId,
571
618
  }),
572
619
  content: formatInboundContent(normalized.rawMessage),
620
+ segments: normalized.content,
573
621
  sender: normalized.userId,
574
622
  channelType: normalized.channelType,
575
623
  metadata: buildIcqqQuoteMetadata(data, {
@@ -583,11 +631,24 @@ export class IcqqIpcEndpoint {
583
631
  if (!payload || typeof payload !== 'object')
584
632
  return;
585
633
  const normalized = normalizeIcqqGuildInboundMessage(payload);
586
- if (!normalized)
634
+ if (!normalized) {
635
+ logger.debug(formatCompact({
636
+ op: 'icqq_inbound_skip',
637
+ reason: 'guild_normalize_failed',
638
+ endpoint: this.name,
639
+ }));
587
640
  return;
641
+ }
588
642
  this.#guildCatalog.upsertFromInbound(normalized.raw);
589
- if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`))
643
+ if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) {
644
+ logger.debug(formatCompact({
645
+ op: 'icqq_inbound_skip',
646
+ reason: 'dedupe',
647
+ endpoint: this.name,
648
+ id: `guild:${normalized.messageId}`,
649
+ }));
590
650
  return;
651
+ }
591
652
  this.admit({
592
653
  id: normalized.messageId,
593
654
  target: formatInboundTarget({
@@ -596,6 +657,7 @@ export class IcqqIpcEndpoint {
596
657
  guildId: normalized.guildId,
597
658
  }),
598
659
  content: formatInboundContent(normalized.rawMessage),
660
+ segments: normalized.content,
599
661
  sender: normalized.userId,
600
662
  channelType: 'channel',
601
663
  metadata: {
@@ -706,6 +768,20 @@ function toNumericId(value, label) {
706
768
  }
707
769
  return n;
708
770
  }
771
+ function summarizeSegmentTypes(segments) {
772
+ if (!segments?.length)
773
+ return undefined;
774
+ return segments
775
+ .map((segment) => {
776
+ if (typeof segment.type === 'string')
777
+ return segment.type;
778
+ if (segment.type && typeof segment.type === 'object' && 'name' in segment.type) {
779
+ return String(segment.type.name);
780
+ }
781
+ return '?';
782
+ })
783
+ .join(',');
784
+ }
709
785
  async function defaultCreateIpc(config) {
710
786
  const uin = Number(config.name);
711
787
  if (config.rpc) {
@@ -0,0 +1 @@
1
+ export declare const icqqEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage>>>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `icqq endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
+ *
4
+ * ICQQ 的凭据由外部守护进程(@icqqjs/cli)按账号持有(~/.icqq/<uin>/),
5
+ * 其 IPC/RPC 协议是「按 uin 连接 + auth + 账号级 action」,没有 add-account /
6
+ * 触发 login 的 action——扫码登录只能经 `icqq login <uin>` CLI 完成。
7
+ * 因此 add 不走 kv 凭据录入,而是 bindFlow:校验 uin、把 { name: '<uin>' }
8
+ * 追加到 plugins.icqq.endpoints,并引导用户先完成守护进程登录。
9
+ */
10
+ import { addEndpointToConfig, createEndpointCommands } from '@zhin.js/adapter';
11
+ import { defineCommand } from '@zhin.js/command';
12
+ import { icqqRuntimeStateToken } from './icqq-runtime-state.js';
13
+ export const icqqEndpointCommands = createEndpointCommands({
14
+ adapterKey: 'icqq',
15
+ adapterDisplayName: 'ICQQ',
16
+ running: (use) => use(icqqRuntimeStateToken).endpoints.values(),
17
+ describeEntry: (entry) => (entry.rpc ? 'rpc(远程守护进程)' : 'ipc(本地守护进程)'),
18
+ addDescription: '登记 ICQQ endpoint(uin 需先经 icqq login 完成扫码登录;重启 zhin 生效)',
19
+ bindFlow: ({ name }) => {
20
+ if (!name) {
21
+ return '用法:icqq endpoint add <uin>(uin 为纯数字 QQ 号,且需先执行 icqq login <uin> 完成扫码登录)';
22
+ }
23
+ if (!/^\d+$/.test(name)) {
24
+ return 'icqq endpoint 名必须是纯数字 QQ 号(uin)';
25
+ }
26
+ try {
27
+ const filePath = addEndpointToConfig('icqq', { name });
28
+ return (`✅ endpoint「${name}」已追加到 ${filePath} 的 plugins.icqq.endpoints。\n` +
29
+ `⚠️ 请确保已执行 icqq login ${name} 启动守护进程并完成扫码登录;重启 zhin 后生效。`);
30
+ }
31
+ catch (error) {
32
+ return `添加失败:${error instanceof Error ? error.message : String(error)}`;
33
+ }
34
+ },
35
+ }, defineCommand);
@@ -0,0 +1 @@
1
+ export declare const icqqRuntimeStateToken: import("zhin.js").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * ICQQ 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `icqq endpoint` 命令共享(同一 owner generation)。
4
+ */
5
+ import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
+ export const icqqRuntimeStateToken = defineEndpointRuntimeStateToken('icqq');
package/lib/ipc-client.js CHANGED
@@ -216,7 +216,7 @@ export class IpcClient {
216
216
  token = (await readFile(getTokenPath(uin), "utf-8")).trim();
217
217
  }
218
218
  catch {
219
- throw new Error("无法读取认证 token,守护进程可能未运行");
219
+ throw new Error(`无法读取认证 token,守护进程可能未运行:请先执行 icqq login ${uin} 完成登录并启动守护进程`);
220
220
  }
221
221
  let rpcInfo;
222
222
  try {
@@ -224,7 +224,7 @@ export class IpcClient {
224
224
  rpcInfo = JSON.parse(raw);
225
225
  }
226
226
  catch {
227
- throw new Error("RPC 未启用或守护进程未运行");
227
+ throw new Error(`RPC 未启用或守护进程未运行:请先执行 icqq login ${uin},并确认守护进程已启用 RPC`);
228
228
  }
229
229
  return IpcClient.connectRpc({
230
230
  host: rpcInfo.host,
@@ -7,7 +7,8 @@ export type IcqqOutboundMediaMode = "file" | "base64";
7
7
  */
8
8
  export declare function resolveIcqqOutboundMediaMode(config: Pick<IcqqEndpointConfig, "rpc" | "outboundMedia">): IcqqOutboundMediaMode;
9
9
  /**
10
- * file 模式:将 segment.data.base64 物化为本机路径。
11
- * base64 模式:不改动(由 toCqString 生成 base64://,供异机守护进程读取)。
10
+ * file 模式:将 segment.data.base64(legacy)或 canonical MediaRef
11
+ * (`data.media`,kind=base64)物化为本机路径,并把 media 改写为 path 引用。
12
+ * base64 模式:不改动(由 toCqString/formatOutboundBody 生成 base64://,供异机守护进程读取)。
12
13
  */
13
14
  export declare function materializeOutboundBase64(content: SendContent, mode?: IcqqOutboundMediaMode): SendContent;
@@ -33,8 +33,9 @@ export function resolveIcqqOutboundMediaMode(config) {
33
33
  return config.rpc ? "base64" : "file";
34
34
  }
35
35
  /**
36
- * file 模式:将 segment.data.base64 物化为本机路径。
37
- * base64 模式:不改动(由 toCqString 生成 base64://,供异机守护进程读取)。
36
+ * file 模式:将 segment.data.base64(legacy)或 canonical MediaRef
37
+ * (`data.media`,kind=base64)物化为本机路径,并把 media 改写为 path 引用。
38
+ * base64 模式:不改动(由 toCqString/formatOutboundBody 生成 base64://,供异机守护进程读取)。
38
39
  */
39
40
  export function materializeOutboundBase64(content, mode = "file") {
40
41
  if (mode === "base64")
@@ -45,16 +46,27 @@ export function materializeOutboundBase64(content, mode = "file") {
45
46
  return seg;
46
47
  const { type, data } = seg;
47
48
  const d = data;
48
- const b64 = d.base64 ?? d.data;
49
- if (typeof b64 !== "string" || !b64)
49
+ const media = d.media;
50
+ const mediaBase64 = media && media.kind === "base64" && typeof media.value === "string" && media.value
51
+ ? media.value.replace(/^base64:\/\//, "")
52
+ : undefined;
53
+ const legacyBase64 = d.base64 ?? d.data;
54
+ const b64 = mediaBase64 ??
55
+ (typeof legacyBase64 === "string" && legacyBase64 ? legacyBase64 : undefined);
56
+ if (!b64)
50
57
  return seg;
51
- const mime = String(d.mime ?? d.mimeType ?? "");
58
+ const mime = String(d.mime ?? d.mimeType ?? (typeof media?.mime_type === "string" ? media.mime_type : "") ?? "");
52
59
  if (type === "image" || type === "record" || type === "audio" || type === "video") {
53
60
  const filePath = spoolBase64ToFile(b64, mime || (type === "image" ? "image/jpeg" : type === "video" ? "video/mp4" : "audio/mpeg"), type === "record" || type === "audio" ? "audio" : type);
54
61
  return {
55
62
  type,
56
63
  data: {
57
64
  ...d,
65
+ media: {
66
+ kind: "path",
67
+ value: filePath,
68
+ ...(mime ? { mime_type: mime } : {}),
69
+ },
58
70
  file: filePath,
59
71
  url: filePath,
60
72
  },
package/lib/protocol.d.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * 登录相关:**推送**走 IpcEvent(`system.login.*`)。
13
13
  */
14
+ import type { MessageSegment } from "zhin.js";
14
15
  /** CLI → Daemon 请求 */
15
16
  export type IpcRequest = {
16
17
  id: string;
@@ -237,6 +238,11 @@ export interface IcqqInboundMessage {
237
238
  readonly id: string;
238
239
  readonly target: string;
239
240
  readonly content: string;
241
+ /**
242
+ * 结构化段视图(canonical Segment,经 toCanonicalSegments 归一)。
243
+ * 与 content(CQ 原文纯文本视图)同源,随 gateway.receive 上送。
244
+ */
245
+ readonly segments?: readonly MessageSegment[];
240
246
  readonly sender: string;
241
247
  readonly channelType: 'private' | 'group' | 'channel';
242
248
  readonly metadata?: Record<string, unknown>;
package/lib/protocol.js CHANGED
@@ -277,16 +277,16 @@ export function formatOutboundBody(payload) {
277
277
  case 'mention':
278
278
  return `[at:${item.data?.qq ?? item.data?.id ?? item.data?.target ?? ''}]`;
279
279
  case 'image':
280
- return `[image:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
280
+ return `[image:${mediaRefValue(item.data) || item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
281
281
  case 'face':
282
282
  return `[face:${item.data?.id ?? ''}]`;
283
283
  case 'reply':
284
284
  return `[reply:${item.data?.message_id ?? item.data?.id ?? ''}]`;
285
285
  case 'record':
286
286
  case 'audio':
287
- return `[record:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
287
+ return `[record:${mediaRefValue(item.data) || item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
288
288
  case 'video':
289
- return `[video:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
289
+ return `[video:${mediaRefValue(item.data) || item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
290
290
  default:
291
291
  return String(item.data?.text ?? '');
292
292
  }
@@ -301,6 +301,23 @@ function base64Media(data) {
301
301
  const b64 = data?.base64;
302
302
  return typeof b64 === 'string' && b64 ? `base64://${b64}` : undefined;
303
303
  }
304
+ /**
305
+ * canonical MediaRef(`data.media`,见 built/segment-contract)→ CQ 媒体值:
306
+ * base64 → `base64://` 前缀(守护进程解码);url/path 直出。
307
+ * file 模式下 materializeOutboundBase64 已把 base64 落盘并把 media 改写为 path 引用。
308
+ */
309
+ function mediaRefValue(data) {
310
+ const media = data?.media;
311
+ if (!media || typeof media !== 'object' || Array.isArray(media))
312
+ return undefined;
313
+ const { kind, value } = media;
314
+ if (typeof value !== 'string' || !value)
315
+ return undefined;
316
+ if (kind === 'base64') {
317
+ return value.startsWith('base64://') ? value : `base64://${value}`;
318
+ }
319
+ return value;
320
+ }
304
321
  /** 段对象判定:`{ type: string, data? }`(非数组 payload 的防御入口)。 */
305
322
  function isWireSegment(value) {
306
323
  return typeof value === 'object'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-icqq",
3
- "version": "7.0.0",
3
+ "version": "7.0.2",
4
4
  "description": "Zhin.js ICQQ adapter for Plugin Runtime (IPC daemon client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,18 +34,19 @@
34
34
  "directory": "plugins/adapters/icqq"
35
35
  },
36
36
  "dependencies": {
37
- "@zhin.js/adapter": "1.1.0",
38
- "@zhin.js/core": "1.4.0",
37
+ "@zhin.js/adapter": "1.1.2",
38
+ "@zhin.js/command": "1.0.4",
39
+ "@zhin.js/core": "1.4.2",
39
40
  "@zhin.js/logger": "1.0.75",
40
- "@zhin.js/plugin-runtime": "1.1.0"
41
+ "@zhin.js/plugin-runtime": "1.1.1"
41
42
  },
42
43
  "peerDependencies": {
43
44
  "zod": "^4.0.0",
44
- "@zhin.js/adapter": "1.1.0",
45
- "@zhin.js/agent": "1.0.5",
46
- "@zhin.js/core": "1.4.0",
47
- "@zhin.js/plugin-runtime": "1.1.0",
48
- "zhin.js": "5.0.0"
45
+ "@zhin.js/adapter": "1.1.2",
46
+ "@zhin.js/agent": "1.0.7",
47
+ "@zhin.js/core": "1.4.2",
48
+ "@zhin.js/plugin-runtime": "1.1.1",
49
+ "zhin.js": "5.0.2"
49
50
  },
50
51
  "peerDependenciesMeta": {
51
52
  "zhin.js": {
@@ -63,12 +64,13 @@
63
64
  "typescript": "^6.0.3",
64
65
  "vitest": "^4.1.10",
65
66
  "zod": "^4.4.3",
66
- "@zhin.js/agent": "1.0.5",
67
- "zhin.js": "5.0.0"
67
+ "@zhin.js/agent": "1.0.7",
68
+ "zhin.js": "5.0.2"
68
69
  },
69
70
  "files": [
70
71
  "adapters",
71
- "plugin.ts",
72
+ "commands",
73
+ "plugin.js",
72
74
  "schema.json",
73
75
  "src",
74
76
  "lib",
@@ -86,13 +88,17 @@
86
88
  "zhin": {
87
89
  "protocol": 1,
88
90
  "type": "plugin",
89
- "entry": "./plugin.ts",
91
+ "entry": "./plugin.js",
90
92
  "engine": "^1.0.0",
91
93
  "runtime": "trusted",
92
94
  "features": [
93
95
  {
94
96
  "package": "@zhin.js/adapter",
95
97
  "api": "^1.0.0"
98
+ },
99
+ {
100
+ "package": "@zhin.js/command",
101
+ "api": "^1.0.0"
96
102
  }
97
103
  ],
98
104
  "plugins": []
package/plugin.js ADDED
@@ -0,0 +1,37 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { createEndpointRuntimeState } from '@zhin.js/adapter';
3
+ import { definePlugin } from '@zhin.js/plugin-runtime';
4
+ import { icqqRuntimeStateToken } from "./lib/icqq-runtime-state.js";
5
+ // Module-level guard: multiple icqq instances (icqq, icqq-2, …) share this
6
+ // module, but the prompt contributor is per-platform — register it once.
7
+ let promptContributorRegistered = false;
8
+ export default definePlugin({
9
+ name: 'icqq',
10
+ metadata: {
11
+ displayName: 'ICQQ Adapter',
12
+ },
13
+ setup(context) {
14
+ // 运行中 endpoint 注册表(icqq endpoint list 的"运行中"数据源)
15
+ context.resources.provide(icqqRuntimeStateToken, createEndpointRuntimeState());
16
+ // Agent prompt contributor (orchestrator/deferred-worker ICQQ guidance).
17
+ // `zhin.js/agent` is an optional peer — skip silently on IM-only installs.
18
+ if (promptContributorRegistered)
19
+ return;
20
+ let cancelled = false;
21
+ let unregister;
22
+ void Promise.all([
23
+ import('zhin.js/agent'),
24
+ import("./lib/agent-prompt.js"),
25
+ ]).then(([agent, prompt]) => {
26
+ if (cancelled || promptContributorRegistered)
27
+ return;
28
+ agent.registerAgentPromptContributor(prompt.createIcqqAgentPromptContributor());
29
+ promptContributorRegistered = true;
30
+ unregister = () => agent.unregisterAgentPromptContributor('icqq');
31
+ }).catch(() => { });
32
+ return () => {
33
+ cancelled = true;
34
+ unregister?.();
35
+ };
36
+ },
37
+ });
package/src/endpoint.ts CHANGED
@@ -624,13 +624,38 @@ export class IcqqIpcEndpoint implements EndpointInstance {
624
624
 
625
625
  /** Test / internal: admit when open. */
626
626
  admit(msg: IcqqInboundMessage): void {
627
- if (!this.#open) return;
628
- // 新 Runtime Message.content 为纯文本:@ 本机(uin = name)只能经 metadata 传递
629
- const mentioned = isIcqqBotMentioned({ uin: this.name, rawMessage: msg.content });
627
+ if (!this.#open) {
628
+ logger.debug(formatCompact({
629
+ op: 'icqq_inbound_skip',
630
+ reason: 'endpoint_closed',
631
+ endpoint: this.name,
632
+ id: msg.id,
633
+ target: msg.target,
634
+ }));
635
+ return;
636
+ }
637
+ // 新 Runtime Message.content 为纯文本:@ 本机(uin = name)经结构化段 / metadata 传递
638
+ const mentioned = isIcqqBotMentioned({
639
+ uin: this.name,
640
+ content: msg.segments,
641
+ rawMessage: msg.content,
642
+ });
643
+ logger.debug(formatCompact({
644
+ op: 'icqq_inbound_admit',
645
+ endpoint: this.name,
646
+ id: msg.id,
647
+ target: msg.target,
648
+ sender: msg.sender,
649
+ channelType: msg.channelType,
650
+ mentioned: mentioned || undefined,
651
+ preview: msg.content.slice(0, 80),
652
+ segments: summarizeSegmentTypes(msg.segments),
653
+ }));
630
654
  void this.#options.gateway.receive({
631
655
  adapter: this.#options.id,
632
656
  target: msg.target,
633
657
  content: msg.content,
658
+ ...(msg.segments ? { segments: msg.segments } : {}),
634
659
  sender: msg.sender,
635
660
  id: msg.id,
636
661
  metadata: Object.freeze({
@@ -684,10 +709,36 @@ export class IcqqIpcEndpoint implements EndpointInstance {
684
709
  }
685
710
  if (!isIcqqMessagePostType(payload)) return;
686
711
  const data = payload as IcqqIpcMessageEvent;
687
- if (shouldSkipSelfInboundMessage(data)) return;
712
+ if (shouldSkipSelfInboundMessage(data)) {
713
+ logger.debug(formatCompact({
714
+ op: 'icqq_inbound_skip',
715
+ reason: 'self',
716
+ endpoint: this.name,
717
+ user_id: data.user_id,
718
+ self_id: data.self_id,
719
+ }));
720
+ return;
721
+ }
688
722
  const normalized = normalizeIcqqInboundMessage(data);
689
- if (!normalized) return;
690
- if (!this.#inboundDeduper.shouldProcess(normalized.messageId)) return;
723
+ if (!normalized) {
724
+ logger.debug(formatCompact({
725
+ op: 'icqq_inbound_skip',
726
+ reason: 'normalize_failed',
727
+ endpoint: this.name,
728
+ message_id: data.message_id,
729
+ message_type: data.message_type ?? data.type,
730
+ }));
731
+ return;
732
+ }
733
+ if (!this.#inboundDeduper.shouldProcess(normalized.messageId)) {
734
+ logger.debug(formatCompact({
735
+ op: 'icqq_inbound_skip',
736
+ reason: 'dedupe',
737
+ endpoint: this.name,
738
+ id: normalized.messageId,
739
+ }));
740
+ return;
741
+ }
691
742
  this.admit({
692
743
  id: normalized.messageId,
693
744
  target: formatInboundTarget({
@@ -696,6 +747,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
696
747
  channelParentGroupId: normalized.channelParentGroupId,
697
748
  }),
698
749
  content: formatInboundContent(normalized.rawMessage),
750
+ segments: normalized.content,
699
751
  sender: normalized.userId,
700
752
  channelType: normalized.channelType,
701
753
  metadata: buildIcqqQuoteMetadata(data, {
@@ -711,9 +763,24 @@ export class IcqqIpcEndpoint implements EndpointInstance {
711
763
  const normalized = normalizeIcqqGuildInboundMessage(
712
764
  payload as Parameters<typeof normalizeIcqqGuildInboundMessage>[0],
713
765
  );
714
- if (!normalized) return;
766
+ if (!normalized) {
767
+ logger.debug(formatCompact({
768
+ op: 'icqq_inbound_skip',
769
+ reason: 'guild_normalize_failed',
770
+ endpoint: this.name,
771
+ }));
772
+ return;
773
+ }
715
774
  this.#guildCatalog.upsertFromInbound(normalized.raw);
716
- if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) return;
775
+ if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) {
776
+ logger.debug(formatCompact({
777
+ op: 'icqq_inbound_skip',
778
+ reason: 'dedupe',
779
+ endpoint: this.name,
780
+ id: `guild:${normalized.messageId}`,
781
+ }));
782
+ return;
783
+ }
717
784
  this.admit({
718
785
  id: normalized.messageId,
719
786
  target: formatInboundTarget({
@@ -722,6 +789,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
722
789
  guildId: normalized.guildId,
723
790
  }),
724
791
  content: formatInboundContent(normalized.rawMessage),
792
+ segments: normalized.content,
725
793
  sender: normalized.userId,
726
794
  channelType: 'channel',
727
795
  metadata: {
@@ -834,6 +902,21 @@ function toNumericId(value: number | string, label: string): number {
834
902
  return n;
835
903
  }
836
904
 
905
+ function summarizeSegmentTypes(
906
+ segments: readonly { readonly type?: unknown }[] | undefined,
907
+ ): string | undefined {
908
+ if (!segments?.length) return undefined;
909
+ return segments
910
+ .map((segment) => {
911
+ if (typeof segment.type === 'string') return segment.type;
912
+ if (segment.type && typeof segment.type === 'object' && 'name' in segment.type) {
913
+ return String((segment.type as { name: unknown }).name);
914
+ }
915
+ return '?';
916
+ })
917
+ .join(',');
918
+ }
919
+
837
920
  async function defaultCreateIpc(config: ResolvedIcqqConfig): Promise<IcqqIpcTransport> {
838
921
  const uin = Number(config.name);
839
922
  if (config.rpc) {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `icqq endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
+ *
4
+ * ICQQ 的凭据由外部守护进程(@icqqjs/cli)按账号持有(~/.icqq/<uin>/),
5
+ * 其 IPC/RPC 协议是「按 uin 连接 + auth + 账号级 action」,没有 add-account /
6
+ * 触发 login 的 action——扫码登录只能经 `icqq login <uin>` CLI 完成。
7
+ * 因此 add 不走 kv 凭据录入,而是 bindFlow:校验 uin、把 { name: '<uin>' }
8
+ * 追加到 plugins.icqq.endpoints,并引导用户先完成守护进程登录。
9
+ */
10
+ import { addEndpointToConfig, createEndpointCommands } from '@zhin.js/adapter';
11
+ import { defineCommand } from '@zhin.js/command';
12
+ import { icqqRuntimeStateToken } from './icqq-runtime-state.js';
13
+
14
+ export const icqqEndpointCommands = createEndpointCommands({
15
+ adapterKey: 'icqq',
16
+ adapterDisplayName: 'ICQQ',
17
+ running: (use) => use(icqqRuntimeStateToken).endpoints.values(),
18
+ describeEntry: (entry) => (entry.rpc ? 'rpc(远程守护进程)' : 'ipc(本地守护进程)'),
19
+ addDescription: '登记 ICQQ endpoint(uin 需先经 icqq login 完成扫码登录;重启 zhin 生效)',
20
+ bindFlow: ({ name }) => {
21
+ if (!name) {
22
+ return '用法:icqq endpoint add <uin>(uin 为纯数字 QQ 号,且需先执行 icqq login <uin> 完成扫码登录)';
23
+ }
24
+ if (!/^\d+$/.test(name)) {
25
+ return 'icqq endpoint 名必须是纯数字 QQ 号(uin)';
26
+ }
27
+ try {
28
+ const filePath = addEndpointToConfig('icqq', { name });
29
+ return (
30
+ `✅ endpoint「${name}」已追加到 ${filePath} 的 plugins.icqq.endpoints。\n` +
31
+ `⚠️ 请确保已执行 icqq login ${name} 启动守护进程并完成扫码登录;重启 zhin 后生效。`
32
+ );
33
+ } catch (error) {
34
+ return `添加失败:${error instanceof Error ? error.message : String(error)}`;
35
+ }
36
+ },
37
+ }, defineCommand);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * ICQQ 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `icqq endpoint` 命令共享(同一 owner generation)。
4
+ */
5
+ import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
+
7
+ export const icqqRuntimeStateToken = defineEndpointRuntimeStateToken('icqq');
package/src/ipc-client.ts CHANGED
@@ -266,7 +266,7 @@ export class IpcClient {
266
266
  try {
267
267
  token = (await readFile(getTokenPath(uin), "utf-8")).trim();
268
268
  } catch {
269
- throw new Error("无法读取认证 token,守护进程可能未运行");
269
+ throw new Error(`无法读取认证 token,守护进程可能未运行:请先执行 icqq login ${uin} 完成登录并启动守护进程`);
270
270
  }
271
271
 
272
272
  let rpcInfo: { host: string; port: number };
@@ -274,7 +274,7 @@ export class IpcClient {
274
274
  const raw = await readFile(getRpcPortPath(uin), "utf-8");
275
275
  rpcInfo = JSON.parse(raw);
276
276
  } catch {
277
- throw new Error("RPC 未启用或守护进程未运行");
277
+ throw new Error(`RPC 未启用或守护进程未运行:请先执行 icqq login ${uin},并确认守护进程已启用 RPC`);
278
278
  }
279
279
 
280
280
  return IpcClient.connectRpc({
@@ -47,8 +47,9 @@ export function resolveIcqqOutboundMediaMode(
47
47
  }
48
48
 
49
49
  /**
50
- * file 模式:将 segment.data.base64 物化为本机路径。
51
- * base64 模式:不改动(由 toCqString 生成 base64://,供异机守护进程读取)。
50
+ * file 模式:将 segment.data.base64(legacy)或 canonical MediaRef
51
+ * (`data.media`,kind=base64)物化为本机路径,并把 media 改写为 path 引用。
52
+ * base64 模式:不改动(由 toCqString/formatOutboundBody 生成 base64://,供异机守护进程读取)。
52
53
  */
53
54
  export function materializeOutboundBase64(
54
55
  content: SendContent,
@@ -61,10 +62,22 @@ export function materializeOutboundBase64(
61
62
  if (typeof seg === "string") return seg;
62
63
  const { type, data } = seg as MessageSegment;
63
64
  const d = data as Record<string, unknown>;
64
- const b64 = d.base64 ?? d.data;
65
- if (typeof b64 !== "string" || !b64) return seg;
65
+ const media = d.media as
66
+ | { kind?: unknown; value?: unknown; mime_type?: unknown }
67
+ | undefined;
68
+ const mediaBase64 =
69
+ media && media.kind === "base64" && typeof media.value === "string" && media.value
70
+ ? media.value.replace(/^base64:\/\//, "")
71
+ : undefined;
72
+ const legacyBase64 = d.base64 ?? d.data;
73
+ const b64 =
74
+ mediaBase64 ??
75
+ (typeof legacyBase64 === "string" && legacyBase64 ? legacyBase64 : undefined);
76
+ if (!b64) return seg;
66
77
 
67
- const mime = String(d.mime ?? d.mimeType ?? "");
78
+ const mime = String(
79
+ d.mime ?? d.mimeType ?? (typeof media?.mime_type === "string" ? media.mime_type : "") ?? "",
80
+ );
68
81
  if (type === "image" || type === "record" || type === "audio" || type === "video") {
69
82
  const filePath = spoolBase64ToFile(
70
83
  b64,
@@ -75,6 +88,11 @@ export function materializeOutboundBase64(
75
88
  type,
76
89
  data: {
77
90
  ...d,
91
+ media: {
92
+ kind: "path",
93
+ value: filePath,
94
+ ...(mime ? { mime_type: mime } : {}),
95
+ },
78
96
  file: filePath,
79
97
  url: filePath,
80
98
  },
package/src/protocol.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { pickCredential } from '@zhin.js/adapter';
16
+ import type { MessageSegment } from "zhin.js";
16
17
 
17
18
  /** CLI → Daemon 请求 */
18
19
  export type IpcRequest = {
@@ -313,6 +314,11 @@ export interface IcqqInboundMessage {
313
314
  readonly id: string;
314
315
  readonly target: string;
315
316
  readonly content: string;
317
+ /**
318
+ * 结构化段视图(canonical Segment,经 toCanonicalSegments 归一)。
319
+ * 与 content(CQ 原文纯文本视图)同源,随 gateway.receive 上送。
320
+ */
321
+ readonly segments?: readonly MessageSegment[];
316
322
  readonly sender: string;
317
323
  readonly channelType: 'private' | 'group' | 'channel';
318
324
  readonly metadata?: Record<string, unknown>;
@@ -422,16 +428,16 @@ export function formatOutboundBody(payload: unknown): string {
422
428
  case 'mention':
423
429
  return `[at:${item.data?.qq ?? item.data?.id ?? item.data?.target ?? ''}]`;
424
430
  case 'image':
425
- return `[image:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
431
+ return `[image:${mediaRefValue(item.data) || item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
426
432
  case 'face':
427
433
  return `[face:${item.data?.id ?? ''}]`;
428
434
  case 'reply':
429
435
  return `[reply:${item.data?.message_id ?? item.data?.id ?? ''}]`;
430
436
  case 'record':
431
437
  case 'audio':
432
- return `[record:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
438
+ return `[record:${mediaRefValue(item.data) || item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
433
439
  case 'video':
434
- return `[video:${item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
440
+ return `[video:${mediaRefValue(item.data) || item.data?.file || item.data?.url || base64Media(item.data) || ''}]`;
435
441
  default:
436
442
  return String(item.data?.text ?? '');
437
443
  }
@@ -449,6 +455,22 @@ function base64Media(data: Record<string, unknown> | undefined): string | undefi
449
455
  return typeof b64 === 'string' && b64 ? `base64://${b64}` : undefined;
450
456
  }
451
457
 
458
+ /**
459
+ * canonical MediaRef(`data.media`,见 built/segment-contract)→ CQ 媒体值:
460
+ * base64 → `base64://` 前缀(守护进程解码);url/path 直出。
461
+ * file 模式下 materializeOutboundBase64 已把 base64 落盘并把 media 改写为 path 引用。
462
+ */
463
+ function mediaRefValue(data: Record<string, unknown> | undefined): string | undefined {
464
+ const media = data?.media;
465
+ if (!media || typeof media !== 'object' || Array.isArray(media)) return undefined;
466
+ const { kind, value } = media as { kind?: unknown; value?: unknown };
467
+ if (typeof value !== 'string' || !value) return undefined;
468
+ if (kind === 'base64') {
469
+ return value.startsWith('base64://') ? value : `base64://${value}`;
470
+ }
471
+ return value;
472
+ }
473
+
452
474
  /** 段对象判定:`{ type: string, data? }`(非数组 payload 的防御入口)。 */
453
475
  function isWireSegment(value: unknown): value is IcqqWireSegment {
454
476
  return typeof value === 'object'
package/plugin.ts DELETED
@@ -1,32 +0,0 @@
1
- import { definePlugin } from '@zhin.js/plugin-runtime';
2
-
3
- // Module-level guard: multiple icqq instances (icqq, icqq-2, …) share this
4
- // module, but the prompt contributor is per-platform — register it once.
5
- let promptContributorRegistered = false;
6
-
7
- export default definePlugin({
8
- name: 'icqq',
9
- metadata: {
10
- displayName: 'ICQQ Adapter',
11
- },
12
- setup() {
13
- // Agent prompt contributor (orchestrator/deferred-worker ICQQ guidance).
14
- // `zhin.js/agent` is an optional peer — skip silently on IM-only installs.
15
- if (promptContributorRegistered) return;
16
- let cancelled = false;
17
- let unregister: (() => void) | undefined;
18
- void Promise.all([
19
- import('zhin.js/agent'),
20
- import('./src/agent-prompt.js'),
21
- ]).then(([agent, prompt]) => {
22
- if (cancelled || promptContributorRegistered) return;
23
- agent.registerAgentPromptContributor(prompt.createIcqqAgentPromptContributor());
24
- promptContributorRegistered = true;
25
- unregister = () => agent.unregisterAgentPromptContributor('icqq');
26
- }).catch(() => { /* optional peer not installed */ });
27
- return () => {
28
- cancelled = true;
29
- unregister?.();
30
- };
31
- },
32
- });