@zhin.js/adapter-icqq 6.0.3 → 7.0.1

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/src/endpoint.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * IcqqIpcEndpoint — lifecycle, outbound, IPC subscribe/admit for ICQQ daemon.
3
3
  */
4
- import type { EndpointInstance } from '@zhin.js/adapter';
4
+ import type { EndpointInstance, EndpointManagement } from '@zhin.js/adapter';
5
5
  import type { MessageGateway } from '@zhin.js/core/runtime';
6
6
  import { formatCompact, getLogger } from '@zhin.js/logger';
7
7
  import type { CapabilityId } from '@zhin.js/plugin-runtime';
@@ -19,6 +19,14 @@ import {
19
19
  type IcqqIpcMessageEvent,
20
20
  } from './icqq-inbound.js';
21
21
  import {
22
+ buildIcqqInboxNoticeRow,
23
+ buildIcqqInboxRequestRow,
24
+ buildIcqqSystemRequestRow,
25
+ isIcqqNoticePayload,
26
+ isIcqqRequestPayload,
27
+ } from './icqq-inbox.js';
28
+ import {
29
+ IcqqGuildCatalog,
22
30
  isIcqqGuildIpcEvent,
23
31
  normalizeIcqqGuildInboundMessage,
24
32
  } from './icqq-guild.js';
@@ -38,7 +46,7 @@ import {
38
46
  type IpcResponse,
39
47
  type ResolvedIcqqConfig,
40
48
  } from './protocol.js';
41
- import type { IpcFriendInfo, IpcGroupInfo } from './types.js';
49
+ import type { IpcFriendInfo, IpcGroupInfo, IpcMemberInfo, IpcSystemMessage } from './types.js';
42
50
 
43
51
  const logger = getLogger('icqq');
44
52
 
@@ -56,11 +64,23 @@ export interface IcqqIpcTransport {
56
64
 
57
65
  export type CreateIcqqIpc = (config: ResolvedIcqqConfig) => Promise<IcqqIpcTransport>;
58
66
 
67
+ /**
68
+ * 收件箱写/推钩子(由装配层注入,见 adapters/icqq.ts)。
69
+ * record* 写 unified_inbox_request/notice;publish 向 console hub 推送实时事件。
70
+ */
71
+ export interface IcqqInboxHooks {
72
+ recordRequest(row: Record<string, unknown>): void | Promise<void>;
73
+ recordNotice(row: Record<string, unknown>): void | Promise<void>;
74
+ publish?(type: string, data: unknown): void;
75
+ }
76
+
59
77
  export interface IcqqEndpointOptions {
60
78
  readonly id: CapabilityId;
61
79
  readonly gateway: MessageGateway;
62
80
  readonly config: ResolvedIcqqConfig;
63
81
  readonly createIpc?: CreateIcqqIpc;
82
+ /** 未注入时 request/notice 事件仅按非消息载荷忽略(不报错)。 */
83
+ readonly inbox?: IcqqInboxHooks;
64
84
  }
65
85
 
66
86
  export class IcqqIpcEndpoint implements EndpointInstance {
@@ -71,10 +91,28 @@ export class IcqqIpcEndpoint implements EndpointInstance {
71
91
  ipc!: IcqqIpcTransport;
72
92
  readonly friends = new Map<number, IpcFriendInfo>();
73
93
  readonly groups = new Map<number, IpcGroupInfo>();
94
+ readonly #guildCatalog = new IcqqGuildCatalog();
95
+ readonly management: EndpointManagement = Object.freeze<EndpointManagement>({
96
+ listFriends: () => this.getFriendList(),
97
+ listGroups: () => this.getGroupList(),
98
+ listChannels: async () => {
99
+ await this.#guildCatalog.syncAll(this.ipc);
100
+ return this.#guildCatalog.getGuildChannelList();
101
+ },
102
+ listGroupMembers: (groupId) => this.getGroupMemberList(groupId),
103
+ approveRequest: (requestId, remark) => this.approveRequest(requestId, remark),
104
+ rejectRequest: (requestId, reason) => this.rejectRequest(requestId, reason),
105
+ kickGroupMember: (groupId, userId) => this.removeMember(groupId, userId),
106
+ muteGroupMember: (groupId, userId, duration) => this.muteMember(groupId, userId, duration),
107
+ setGroupAdmin: (groupId, userId, enabled) => this.setModerator(groupId, userId, enabled),
108
+ deleteFriend: (userId) => this.deleteFriend(userId),
109
+ });
74
110
  #open = false;
75
111
  #started = false;
76
112
  #subscriptions: Array<{ unsubscribe: () => Promise<void> }> = [];
77
113
  #inboundDeduper = new InboundMessageDeduper();
114
+ /** request/notice 去重(推送事件与 GET_SYSTEM_MSG 首拉可能重复同一 flag)。 */
115
+ #inboxDeduper = new InboundMessageDeduper();
78
116
  #unregisterAgent?: () => void;
79
117
  /** 用户主动 stop 时为 true,阻止自动重连 */
80
118
  #intentionalDisconnect = false;
@@ -130,6 +168,9 @@ export class IcqqIpcEndpoint implements EndpointInstance {
130
168
  this.#handleEvent(event);
131
169
  });
132
170
  this.#subscriptions.push(sub);
171
+ // daemon 推送 client.em 全部事件,request/notice 实时到达;
172
+ // 启动时补一次 GET_SYSTEM_MSG,捞离线期间积存的待处理请求。
173
+ void this.#pullPendingSystemMessages();
133
174
  }
134
175
 
135
176
  /** IPC/RPC 意外断开时调度重连(指数退避) */
@@ -203,8 +244,10 @@ export class IcqqIpcEndpoint implements EndpointInstance {
203
244
  this.#unregisterAgent?.();
204
245
  this.#unregisterAgent = undefined;
205
246
  this.#inboundDeduper.clear();
247
+ this.#inboxDeduper.clear();
206
248
  this.friends.clear();
207
249
  this.groups.clear();
250
+ this.#guildCatalog.clear();
208
251
  try {
209
252
  this.ipc?.close();
210
253
  } catch {
@@ -404,15 +447,215 @@ export class IcqqIpcEndpoint implements EndpointInstance {
404
447
  }
405
448
  }
406
449
 
450
+ // ── 社交/群管(Console endpoint RPC 探测面)────────────────────────
451
+ // console-rpc-extended.ts 按方法名探测这些接口;全部薄封装走 IPC daemon。
452
+
453
+ /** EndpointManagement.listFriends —— 好友列表(LIST_FRIENDS,归一为 {user_id, nickname, remark})。 */
454
+ async getFriendList(): Promise<Array<{ user_id: number; nickname: string; remark: string }>> {
455
+ const resp = await this.#mustRequest(Actions.LIST_FRIENDS, undefined, '获取好友列表');
456
+ const list = Array.isArray(resp.data) ? (resp.data as IpcFriendInfo[]) : [];
457
+ return list.map((f) => ({
458
+ user_id: f.user_id,
459
+ nickname: f.nickname,
460
+ remark: f.remark ?? '',
461
+ }));
462
+ }
463
+
464
+ /** EndpointManagement.listGroups —— 群列表(LIST_GROUPS,归一为 {group_id, name})。 */
465
+ async getGroupList(): Promise<Array<{ group_id: number; name: string }>> {
466
+ const resp = await this.#mustRequest(Actions.LIST_GROUPS, undefined, '获取群列表');
467
+ const list = Array.isArray(resp.data) ? (resp.data as IpcGroupInfo[]) : [];
468
+ return list.map((g) => ({
469
+ group_id: g.group_id,
470
+ name: g.group_name,
471
+ }));
472
+ }
473
+
474
+ /** EndpointManagement.listGroupMembers —— 群成员列表(字段对齐 daemon 返回)。 */
475
+ async getGroupMemberList(groupId: number | string): Promise<IpcMemberInfo[]> {
476
+ const resp = await this.#mustRequest(
477
+ Actions.LIST_GROUP_MEMBERS,
478
+ { group_id: toNumericId(groupId, 'group_id') },
479
+ '获取群成员列表',
480
+ );
481
+ return Array.isArray(resp.data) ? (resp.data as IpcMemberInfo[]) : [];
482
+ }
483
+
484
+ /** getGroupMemberList 别名(console 探测 listMembers / getMemberList)。 */
485
+ listMembers(groupId: number | string): Promise<IpcMemberInfo[]> {
486
+ return this.getGroupMemberList(groupId);
487
+ }
488
+
489
+ getMemberList(groupId: number | string): Promise<IpcMemberInfo[]> {
490
+ return this.getGroupMemberList(groupId);
491
+ }
492
+
493
+ /**
494
+ * EndpointManagement.resolveRequest —— id 为 inbox 行的 platform_request_id。
495
+ * 先 GET_SYSTEM_MSG 按 flag(回退 seq)定位请求,再按类型路由 handle_friend/group_request。
496
+ */
497
+ async approveRequest(id: string, remark?: string): Promise<void> {
498
+ await this.#handleSystemRequest(id, true, { remark });
499
+ }
500
+
501
+ async rejectRequest(id: string, reason?: string): Promise<void> {
502
+ await this.#handleSystemRequest(id, false, { reason });
503
+ }
504
+
505
+ /** EndpointManagement.deleteFriend —— FRIEND_DELETE。 */
506
+ async deleteFriend(userId: number | string): Promise<void> {
507
+ await this.#mustRequest(
508
+ Actions.FRIEND_DELETE,
509
+ { user_id: toNumericId(userId, 'user_id') },
510
+ '删除好友',
511
+ );
512
+ }
513
+
514
+ /** deleteFriend 别名(console 探测 delete_friend)。 */
515
+ delete_friend(userId: number | string): Promise<void> {
516
+ return this.deleteFriend(userId);
517
+ }
518
+
519
+ /** EndpointManagement.kickGroupMember —— GROUP_KICK。 */
520
+ async removeMember(groupId: number | string, userId: number | string): Promise<void> {
521
+ await this.#mustRequest(
522
+ Actions.GROUP_KICK,
523
+ { group_id: toNumericId(groupId, 'group_id'), user_id: toNumericId(userId, 'user_id') },
524
+ '踢出群成员',
525
+ );
526
+ }
527
+
528
+ kickMember(groupId: number | string, userId: number | string): Promise<void> {
529
+ return this.removeMember(groupId, userId);
530
+ }
531
+
532
+ setGroupKick(groupId: number | string, userId: number | string): Promise<void> {
533
+ return this.removeMember(groupId, userId);
534
+ }
535
+
536
+ /** EndpointManagement.muteGroupMember —— GROUP_MUTE(duration 秒,默认 600)。 */
537
+ async muteMember(groupId: number | string, userId: number | string, duration = 600): Promise<void> {
538
+ await this.#mustRequest(
539
+ Actions.GROUP_MUTE,
540
+ {
541
+ group_id: toNumericId(groupId, 'group_id'),
542
+ user_id: toNumericId(userId, 'user_id'),
543
+ duration,
544
+ },
545
+ '禁言群成员',
546
+ );
547
+ }
548
+
549
+ banMember(groupId: number | string, userId: number | string, duration = 600): Promise<void> {
550
+ return this.muteMember(groupId, userId, duration);
551
+ }
552
+
553
+ setGroupMute(groupId: number | string, userId: number | string, duration = 600): Promise<void> {
554
+ return this.muteMember(groupId, userId, duration);
555
+ }
556
+
557
+ /** EndpointManagement.setGroupAdmin —— SET_GROUP_ADMIN(enable 默认 true)。 */
558
+ async setModerator(groupId: number | string, userId: number | string, enable = true): Promise<void> {
559
+ await this.#mustRequest(
560
+ Actions.SET_GROUP_ADMIN,
561
+ {
562
+ group_id: toNumericId(groupId, 'group_id'),
563
+ user_id: toNumericId(userId, 'user_id'),
564
+ enable,
565
+ },
566
+ '设置群管理员',
567
+ );
568
+ }
569
+
570
+ setAdmin(groupId: number | string, userId: number | string, enable = true): Promise<void> {
571
+ return this.setModerator(groupId, userId, enable);
572
+ }
573
+
574
+ setGroupAdmin(groupId: number | string, userId: number | string, enable = true): Promise<void> {
575
+ return this.setModerator(groupId, userId, enable);
576
+ }
577
+
578
+ /** IPC 请求并统一错误上下文:daemon 返回 ok=false 时抛出带操作名的错误。 */
579
+ async #mustRequest(
580
+ action: string,
581
+ params: Record<string, unknown> | undefined,
582
+ label: string,
583
+ ): Promise<IpcResponse> {
584
+ const resp = await this.request(action, params);
585
+ if (!resp.ok) {
586
+ throw new Error(`icqq ${label}失败: ${resp.error ?? 'daemon 未返回错误详情'}`);
587
+ }
588
+ return resp;
589
+ }
590
+
591
+ /** 按 flag/seq 在 GET_SYSTEM_MSG 中定位请求,路由到好友/群请求处理 action。 */
592
+ async #handleSystemRequest(
593
+ id: string,
594
+ approve: boolean,
595
+ extra: { remark?: string; reason?: string },
596
+ ): Promise<void> {
597
+ const resp = await this.#mustRequest(Actions.GET_SYSTEM_MSG, undefined, '获取待处理请求');
598
+ const data = resp.data as
599
+ | { friendRequests?: unknown; groupRequests?: unknown }
600
+ | undefined;
601
+ const friendRequests = Array.isArray(data?.friendRequests)
602
+ ? (data.friendRequests as IpcSystemMessage[])
603
+ : [];
604
+ const groupRequests = Array.isArray(data?.groupRequests)
605
+ ? (data.groupRequests as IpcSystemMessage[])
606
+ : [];
607
+ const matches = (m: IpcSystemMessage): boolean =>
608
+ m.flag === id || (m.seq != null && String(m.seq) === id);
609
+ const friend = friendRequests.find(matches);
610
+ const group = friend ? undefined : groupRequests.find(matches);
611
+ const target = friend ?? group;
612
+ if (!target?.flag) {
613
+ throw new Error(`icqq 未找到待处理请求: ${id}(GET_SYSTEM_MSG 中无匹配 flag/seq)`);
614
+ }
615
+ const params: Record<string, unknown> = { flag: target.flag, approve };
616
+ if (friend) {
617
+ if (extra.remark) params.remark = extra.remark;
618
+ await this.#mustRequest(Actions.HANDLE_FRIEND_REQUEST, params, '处理好友请求');
619
+ } else {
620
+ if (extra.reason) params.reason = extra.reason;
621
+ await this.#mustRequest(Actions.HANDLE_GROUP_REQUEST, params, '处理群请求');
622
+ }
623
+ }
624
+
407
625
  /** Test / internal: admit when open. */
408
626
  admit(msg: IcqqInboundMessage): void {
409
- if (!this.#open) return;
410
- // 新 Runtime Message.content 为纯文本:@ 本机(uin = name)只能经 metadata 传递
411
- 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
+ }));
412
654
  void this.#options.gateway.receive({
413
655
  adapter: this.#options.id,
414
656
  target: msg.target,
415
657
  content: msg.content,
658
+ ...(msg.segments ? { segments: msg.segments } : {}),
416
659
  sender: msg.sender,
417
660
  id: msg.id,
418
661
  metadata: Object.freeze({
@@ -456,12 +699,46 @@ export class IcqqIpcEndpoint implements EndpointInstance {
456
699
  }
457
700
  const payload = unwrapIcqqIpcEventPayload(event);
458
701
  if (!payload || typeof payload !== 'object') return;
702
+ if (isIcqqRequestPayload(payload)) {
703
+ this.#recordInboxRequest(payload as Record<string, unknown>);
704
+ return;
705
+ }
706
+ if (isIcqqNoticePayload(payload)) {
707
+ this.#recordInboxNotice(payload as Record<string, unknown>);
708
+ return;
709
+ }
459
710
  if (!isIcqqMessagePostType(payload)) return;
460
711
  const data = payload as IcqqIpcMessageEvent;
461
- 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
+ }
462
722
  const normalized = normalizeIcqqInboundMessage(data);
463
- if (!normalized) return;
464
- 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
+ }
465
742
  this.admit({
466
743
  id: normalized.messageId,
467
744
  target: formatInboundTarget({
@@ -470,6 +747,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
470
747
  channelParentGroupId: normalized.channelParentGroupId,
471
748
  }),
472
749
  content: formatInboundContent(normalized.rawMessage),
750
+ segments: normalized.content,
473
751
  sender: normalized.userId,
474
752
  channelType: normalized.channelType,
475
753
  metadata: buildIcqqQuoteMetadata(data, {
@@ -485,8 +763,24 @@ export class IcqqIpcEndpoint implements EndpointInstance {
485
763
  const normalized = normalizeIcqqGuildInboundMessage(
486
764
  payload as Parameters<typeof normalizeIcqqGuildInboundMessage>[0],
487
765
  );
488
- if (!normalized) return;
489
- if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) 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
+ }
774
+ this.#guildCatalog.upsertFromInbound(normalized.raw);
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
+ }
490
784
  this.admit({
491
785
  id: normalized.messageId,
492
786
  target: formatInboundTarget({
@@ -495,6 +789,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
495
789
  guildId: normalized.guildId,
496
790
  }),
497
791
  content: formatInboundContent(normalized.rawMessage),
792
+ segments: normalized.content,
498
793
  sender: normalized.userId,
499
794
  channelType: 'channel',
500
795
  metadata: {
@@ -503,6 +798,68 @@ export class IcqqIpcEndpoint implements EndpointInstance {
503
798
  },
504
799
  });
505
800
  }
801
+
802
+ /** 收件箱行公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
803
+ #inboxBase(): { adapter: string; endpointId: string } {
804
+ const id = String(this.#options.id);
805
+ return { adapter: id.split('\0').pop() ?? id, endpointId: this.name };
806
+ }
807
+
808
+ #recordInboxRequest(payload: Record<string, unknown>): void {
809
+ const hooks = this.#options.inbox;
810
+ if (!hooks) return;
811
+ const row = buildIcqqInboxRequestRow(payload, this.#inboxBase());
812
+ if (!row) return;
813
+ if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`)) return;
814
+ void hooks.recordRequest(row);
815
+ hooks.publish?.('endpoint:request', row);
816
+ }
817
+
818
+ #recordInboxNotice(payload: Record<string, unknown>): void {
819
+ const hooks = this.#options.inbox;
820
+ if (!hooks) return;
821
+ const row = buildIcqqInboxNoticeRow(payload, this.#inboxBase());
822
+ if (!row) return;
823
+ if (!this.#inboxDeduper.shouldProcess(`notice:${String(row.platform_notice_id)}`)) return;
824
+ void hooks.recordNotice(row);
825
+ hooks.publish?.('endpoint:notice', row);
826
+ }
827
+
828
+ /** 启动/重连后首拉 GET_SYSTEM_MSG:补录离线期间的好友/群待处理请求。 */
829
+ async #pullPendingSystemMessages(): Promise<void> {
830
+ const hooks = this.#options.inbox;
831
+ if (!hooks || !this.ipc) return;
832
+ try {
833
+ const resp = await this.ipc.request(Actions.GET_SYSTEM_MSG);
834
+ if (!resp.ok) return;
835
+ const data = resp.data as
836
+ | { friendRequests?: unknown; groupRequests?: unknown }
837
+ | undefined;
838
+ const base = this.#inboxBase();
839
+ const rows = [
840
+ ...(Array.isArray(data?.friendRequests)
841
+ ? (data.friendRequests as IpcSystemMessage[]).map((m) =>
842
+ buildIcqqSystemRequestRow(m, 'friend', base))
843
+ : []),
844
+ ...(Array.isArray(data?.groupRequests)
845
+ ? (data.groupRequests as IpcSystemMessage[]).map((m) =>
846
+ buildIcqqSystemRequestRow(m, 'group', base))
847
+ : []),
848
+ ];
849
+ for (const row of rows) {
850
+ if (!row) continue;
851
+ if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`)) continue;
852
+ void hooks.recordRequest(row);
853
+ hooks.publish?.('endpoint:request', row);
854
+ }
855
+ } catch (error) {
856
+ logger.debug(formatCompact({
857
+ op: 'inbox_pull_system_msg',
858
+ endpoint: this.name,
859
+ error: error instanceof Error ? error.message : String(error),
860
+ }));
861
+ }
862
+ }
506
863
  }
507
864
 
508
865
  /**
@@ -536,6 +893,30 @@ function buildIcqqQuoteMetadata(
536
893
  return metadata;
537
894
  }
538
895
 
896
+ /** console RPC 传入的 gid/uid 可能是字符串,统一收敛为数字。 */
897
+ function toNumericId(value: number | string, label: string): number {
898
+ const n = Number(value);
899
+ if (!Number.isFinite(n) || String(value).trim() === '') {
900
+ throw new TypeError(`icqq ${label} 必须是数字: ${String(value)}`);
901
+ }
902
+ return n;
903
+ }
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
+
539
920
  async function defaultCreateIpc(config: ResolvedIcqqConfig): Promise<IcqqIpcTransport> {
540
921
  const uin = Number(config.name);
541
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);
package/src/icqq-guild.ts CHANGED
@@ -3,9 +3,16 @@
3
3
  */
4
4
  import { toCanonicalSegments, type MessageSegment } from "zhin.js";
5
5
  import { Actions, type IpcGuildMessageEventData } from "./protocol.js";
6
- import type { IpcClient } from "./ipc-client.js";
7
6
  import { parseCqMessage } from "./cq-message.js";
8
7
 
8
+ /** Narrow request port shared by the real IPC client and endpoint test transports. */
9
+ export interface IcqqGuildRpc {
10
+ request(
11
+ action: string,
12
+ params?: Record<string, unknown>,
13
+ ): Promise<{ ok: boolean; data?: unknown }>;
14
+ }
15
+
9
16
  export interface IcqqGuildChannelEntry {
10
17
  channel_id: string;
11
18
  channel_name: string;
@@ -36,7 +43,7 @@ export class IcqqGuildCatalog {
36
43
  this.guilds.clear();
37
44
  }
38
45
 
39
- async syncAll(ipc: IpcClient): Promise<void> {
46
+ async syncAll(ipc: IcqqGuildRpc): Promise<void> {
40
47
  const listResp = await ipc.request(Actions.GUILD_LIST);
41
48
  if (!listResp.ok) return;
42
49