@zhin.js/adapter-icqq 6.0.3 → 7.0.0

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,30 @@
1
1
  # @zhin.js/adapter-icqq
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 7db69c1: 命令前缀改为适配器配置项:`MessageDispatcher` 不再硬编码 `/`,默认按消息所属适配器实例 config 的 `commandPrefix` 解析(默认 `''` 无前缀,任意文本按命令匹配),`endpoints[i].commandPrefix` 逐项覆盖;`ImRuntime({ commandPrefix })` 仍可设全局静态前缀。全部 20 个平台适配器 schema 新增 `commandPrefix` 属性。
8
+
9
+ BREAKING(行为变化):未配置时命令不再需要 `/` 前缀——原 `/zt` 写法不再命中,直接发 `zt` 即可;需要斜杠风格的适配器请在配置里显式设 `commandPrefix: '/'`。
10
+
11
+ - e5c84ed: Adapter 多账号:插件实例 config 支持 `endpoints: [{name, ...覆盖}]` 数组,`expandEndpointConfigs` 将一个实例展开为多个 endpoint record(id 为 `<slotId>~<name>`,顶层字段共享、逐项覆盖),替代多 `instanceKey` 方案;Console `/api/plugins` 收敛为一个插件卡片 + 多 endpoint。icqq / qq schema 与 README 补 `endpoints` 配置。
12
+
13
+ Plugin Runtime Console Host:补 `/esm/*` React/router ESM 代理路由(legacy `consoleApiRouter` 对齐),TypeScriptClientBuilder 裸导入改写为 `/esm/<enc>.mjs`。
14
+
15
+ - 713445c: 适配器配置格式定稿(不兼容旧格式):`plugins.<adapter>` 顶层仅共享字段 + `commandPrefix`,`endpoints[i]` 携带 endpoint 级字段(`name` + 凭据,各 schema 已类型化),`endpoints` 为必填(icqq 另需顶层 `master`);icqq 新增 `trusted` 列表(顶层/逐项均可)。scaffold-wizard 全部字段式与自定义 configure() 产出改为新格式,examples(full-bot / qq-games-bot)与 20 个适配器 README 同步迁移。
16
+ - f32c424: 适配器配置统一为 endpoints 数组格式:`plugins.<adapter>` 为该 adapter 所有 endpoint 的通用配置,`plugins.<adapter>.endpoints[i]` 为单个 endpoint 的特殊配置(逐项覆盖,`name` 必填)。slack / github schema 新增 `endpoints` 属性;icqq / qq / slack 顶层必填改为 `anyOf`(单 endpoint 字段或 `endpoints` 数组二选一,兼容 Ajv strictRequired)。
17
+ - Updated dependencies [7db69c1]
18
+ - Updated dependencies [e5c84ed]
19
+ - Updated dependencies [3ea84a0]
20
+ - Updated dependencies [1ddcd70]
21
+ - Updated dependencies [ac9da66]
22
+ - @zhin.js/core@1.4.0
23
+ - @zhin.js/adapter@1.1.0
24
+ - @zhin.js/plugin-runtime@1.1.0
25
+ - @zhin.js/agent@1.0.5
26
+ - zhin.js@5.0.0
27
+
3
28
  ## 6.0.3
4
29
 
5
30
  ### Patch Changes
package/README.md CHANGED
@@ -8,6 +8,7 @@ ICQQ Plugin Runtime 适配器 — 通过 [@icqqjs/cli](https://github.com/icqqjs
8
8
  - 入站:`messageGatewayToken`(IPC 事件订阅)
9
9
  - 出站:`send({ target, payload })` → `send_group_msg` / `send_private_msg` / …
10
10
  - Agent 工具:`agent/tools/`(戳一戳、群管、好友列表等)保留
11
+ - Console Endpoint 管理:`src/endpoint.ts` 显式实现 `EndpointManagement`(好友/群/群成员列表、请求审批、删好友、踢人、禁言、设管理),Console 使用 `endpoint.friends` / `endpoint.groups` / `endpoint.group_members` 等规范 RPC
11
12
 
12
13
  ## 安装
13
14
 
@@ -21,12 +22,28 @@ pnpm add -g @icqqjs/cli # 或 npx icqq login
21
22
  ```yaml
22
23
  plugins:
23
24
  icqq:
24
- name: "${ICQQ_ACCOUNT}" # QQ 号,须与 icqq login 一致
25
+ master: "1659488338" # 必填,顶层共享(/approve 与 master 角色)
25
26
  autoReconnect: true
26
- # rpc: # 可选远程守护进程
27
- # host: 10.0.0.2
28
- # port: 9527
29
- # token: ${ICQQ_RPC_TOKEN}
27
+ endpoints:
28
+ - name: "${ICQQ_ACCOUNT}" # QQ 号,须与 icqq login 一致
29
+ # rpc 为 endpoint 级字段:
30
+ # - name: "${ICQQ_ACCOUNT_2}"
31
+ # rpc:
32
+ # host: 10.0.0.2
33
+ # port: 9527
34
+ # token: ${ICQQ_RPC_TOKEN}
35
+ ```
36
+
37
+ 多账号:一个插件实例挂多个 endpoint(`endpoints` 数组逐项覆盖顶层字段,`name` 必填):
38
+
39
+ ```yaml
40
+ plugins:
41
+ icqq:
42
+ master: "1659488338" # 顶层字段所有 endpoint 共享
43
+ endpoints:
44
+ - name: "${ICQQ_ACCOUNT}"
45
+ - name: "${ICQQ_ACCOUNT_2}"
46
+ - name: "${ICQQ_ACCOUNT_3}" # 各账号须分别 icqq login
30
47
  ```
31
48
 
32
49
  先执行 `icqq login`,再启动 Zhin。
@@ -52,6 +69,7 @@ plugins:
52
69
 
53
70
  - `autoReconnect` 已恢复实现:IPC/RPC 意外断开后按指数退避自动重连(`stop()` 为主动断开,不触发重连)。
54
71
  - `outboundMedia: file | base64` 已恢复实现:`file` 模式把 segment base64 落盘为临时文件后发 `[image:path]`;`base64` 模式(配置 `rpc` 时默认)发 `[image:base64://...]` 由守护进程解码。
72
+ - **Console 社交/群管 RPC 已接线**:endpoint 在 Adapter 内把 ICQQ 的 `get_friend_list` / `get_group_list` / `get_group_member_list`、请求审批和群管操作归一化为冻结的 `EndpointManagement` 对象。Host 只消费该语义端口,不再探测方法别名或读取 `friends` / `groups` SDK 缓存。
55
73
  - **notice / request 入站事件已移除**:Plugin Runtime 的 `MessageGateway` 仅有 message 通道,没有 `notice.receive` / `request.receive` 事件机制,旧 `icqq-side-events.ts` / `get-msg.ts` / `login-ipc-contract.ts` / `agent-prompt.ts` 随之删除。需要好友/入群请求处理能力时请等待 runtime 提供事件通道后再恢复。
56
74
 
57
75
  ## License
package/adapters/icqq.ts CHANGED
@@ -1,9 +1,17 @@
1
1
  /**
2
2
  * Convention entry: discover `adapters/icqq.ts` → defineAdapter.
3
3
  */
4
- import { defineAdapter } from '@zhin.js/adapter';
4
+ import { defineAdapter, type AdapterContext } from '@zhin.js/adapter';
5
5
  import { messageGatewayToken } from '@zhin.js/core/runtime';
6
- import { IcqqIpcEndpoint } from '../src/endpoint.js';
6
+ import { formatCompact, getLogger } from '@zhin.js/logger';
7
+ import {
8
+ databaseHostToken,
9
+ INBOX_TABLE_NOTICE,
10
+ INBOX_TABLE_REQUEST,
11
+ runtimeEventPublisherToken,
12
+ type Token,
13
+ } from '@zhin.js/plugin-runtime';
14
+ import { IcqqIpcEndpoint, type IcqqInboxHooks } from '../src/endpoint.js';
7
15
  import {
8
16
  resolveIcqqConfig,
9
17
  type IcqqAdapterConfig,
@@ -16,15 +24,69 @@ export type {
16
24
  IcqqIpcTransport,
17
25
  } from '../src/endpoint.js';
18
26
 
27
+ const logger = getLogger('icqq');
28
+
19
29
  export default defineAdapter<IcqqAdapterConfig>({
20
30
  capabilities: ['inbound', 'outbound'],
21
31
  create(context) {
22
32
  // Client-library / IPC daemon path — no httpHostToken.
23
33
  // Console loginAssist + host-router routes deferred.
34
+ const inbox = resolveInboxHooks(context);
24
35
  return new IcqqIpcEndpoint({
25
36
  id: context.id,
26
37
  gateway: context.use(messageGatewayToken),
27
38
  config: resolveIcqqConfig(context.config),
39
+ ...(inbox ? { inbox } : {}),
28
40
  });
29
41
  },
30
42
  });
43
+
44
+ /** 资源可选(轻量 Root 无 DatabaseHost / console hub):缺失时降级为不写收件箱。 */
45
+ function tryUse<T>(context: AdapterContext<IcqqAdapterConfig>, token: Token<T>): T | undefined {
46
+ try {
47
+ return context.use(token);
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 收件箱钩子:写 unified_inbox_request/notice(按 platform_*_id 去重,
55
+ * 推送事件与 GET_SYSTEM_MSG 首拉可能重复同一请求),并经 console hub 广播。
56
+ */
57
+ function resolveInboxHooks(context: AdapterContext<IcqqAdapterConfig>): IcqqInboxHooks | undefined {
58
+ const host = tryUse(context, databaseHostToken);
59
+ const publisher = tryUse(context, runtimeEventPublisherToken);
60
+ if (!host && !publisher) return undefined;
61
+ const insertDeduped = async (
62
+ table: string,
63
+ idField: 'platform_request_id' | 'platform_notice_id',
64
+ row: Record<string, unknown>,
65
+ ): Promise<void> => {
66
+ const model = host?.models.get(table);
67
+ if (!model) return;
68
+ try {
69
+ const existing = await model.select().where({
70
+ adapter: row.adapter,
71
+ endpoint_id: row.endpoint_id,
72
+ [idField]: row[idField],
73
+ });
74
+ if (Array.isArray(existing) && existing.length > 0) return;
75
+ await model.insert(row);
76
+ } catch (error) {
77
+ logger.warn(formatCompact({
78
+ op: 'inbox_insert',
79
+ table,
80
+ error: error instanceof Error ? error.message : String(error),
81
+ }));
82
+ }
83
+ };
84
+ const publish = publisher;
85
+ return {
86
+ recordRequest: (row) => insertDeduped(INBOX_TABLE_REQUEST, 'platform_request_id', row),
87
+ recordNotice: (row) => insertDeduped(INBOX_TABLE_NOTICE, 'platform_notice_id', row),
88
+ ...(publish
89
+ ? { publish: (type: string, data: unknown): void => publish.publish(type, data) }
90
+ : {}),
91
+ };
92
+ }
package/lib/endpoint.d.ts CHANGED
@@ -1,11 +1,11 @@
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 type { CapabilityId } from '@zhin.js/plugin-runtime';
7
7
  import { type IcqqInboundMessage, type IpcEvent, type IpcResponse, type ResolvedIcqqConfig } from './protocol.js';
8
- import type { IpcFriendInfo, IpcGroupInfo } from './types.js';
8
+ import type { IpcFriendInfo, IpcGroupInfo, IpcMemberInfo } from './types.js';
9
9
  /** Minimal IPC surface used by the endpoint (real IpcClient or test mock). */
10
10
  export interface IcqqIpcTransport {
11
11
  request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
@@ -16,11 +16,22 @@ export interface IcqqIpcTransport {
16
16
  close(): void;
17
17
  }
18
18
  export type CreateIcqqIpc = (config: ResolvedIcqqConfig) => Promise<IcqqIpcTransport>;
19
+ /**
20
+ * 收件箱写/推钩子(由装配层注入,见 adapters/icqq.ts)。
21
+ * record* 写 unified_inbox_request/notice;publish 向 console hub 推送实时事件。
22
+ */
23
+ export interface IcqqInboxHooks {
24
+ recordRequest(row: Record<string, unknown>): void | Promise<void>;
25
+ recordNotice(row: Record<string, unknown>): void | Promise<void>;
26
+ publish?(type: string, data: unknown): void;
27
+ }
19
28
  export interface IcqqEndpointOptions {
20
29
  readonly id: CapabilityId;
21
30
  readonly gateway: MessageGateway;
22
31
  readonly config: ResolvedIcqqConfig;
23
32
  readonly createIpc?: CreateIcqqIpc;
33
+ /** 未注入时 request/notice 事件仅按非消息载荷忽略(不报错)。 */
34
+ readonly inbox?: IcqqInboxHooks;
24
35
  }
25
36
  export declare class IcqqIpcEndpoint implements EndpointInstance {
26
37
  #private;
@@ -29,6 +40,7 @@ export declare class IcqqIpcEndpoint implements EndpointInstance {
29
40
  ipc: IcqqIpcTransport;
30
41
  readonly friends: Map<number, IpcFriendInfo>;
31
42
  readonly groups: Map<number, IpcGroupInfo>;
43
+ readonly management: EndpointManagement;
32
44
  constructor(options: IcqqEndpointOptions);
33
45
  request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
34
46
  start(): Promise<void>;
@@ -46,6 +58,44 @@ export declare class IcqqIpcEndpoint implements EndpointInstance {
46
58
  removeReaction(messageId: string, reactionId: string): Promise<void>;
47
59
  /** Activity-feedback autoRemove / Console recall. */
48
60
  recallMessage(messageId: string): Promise<void>;
61
+ /** EndpointManagement.listFriends —— 好友列表(LIST_FRIENDS,归一为 {user_id, nickname, remark})。 */
62
+ getFriendList(): Promise<Array<{
63
+ user_id: number;
64
+ nickname: string;
65
+ remark: string;
66
+ }>>;
67
+ /** EndpointManagement.listGroups —— 群列表(LIST_GROUPS,归一为 {group_id, name})。 */
68
+ getGroupList(): Promise<Array<{
69
+ group_id: number;
70
+ name: string;
71
+ }>>;
72
+ /** EndpointManagement.listGroupMembers —— 群成员列表(字段对齐 daemon 返回)。 */
73
+ getGroupMemberList(groupId: number | string): Promise<IpcMemberInfo[]>;
74
+ /** getGroupMemberList 别名(console 探测 listMembers / getMemberList)。 */
75
+ listMembers(groupId: number | string): Promise<IpcMemberInfo[]>;
76
+ getMemberList(groupId: number | string): Promise<IpcMemberInfo[]>;
77
+ /**
78
+ * EndpointManagement.resolveRequest —— id 为 inbox 行的 platform_request_id。
79
+ * 先 GET_SYSTEM_MSG 按 flag(回退 seq)定位请求,再按类型路由 handle_friend/group_request。
80
+ */
81
+ approveRequest(id: string, remark?: string): Promise<void>;
82
+ rejectRequest(id: string, reason?: string): Promise<void>;
83
+ /** EndpointManagement.deleteFriend —— FRIEND_DELETE。 */
84
+ deleteFriend(userId: number | string): Promise<void>;
85
+ /** deleteFriend 别名(console 探测 delete_friend)。 */
86
+ delete_friend(userId: number | string): Promise<void>;
87
+ /** EndpointManagement.kickGroupMember —— GROUP_KICK。 */
88
+ removeMember(groupId: number | string, userId: number | string): Promise<void>;
89
+ kickMember(groupId: number | string, userId: number | string): Promise<void>;
90
+ setGroupKick(groupId: number | string, userId: number | string): Promise<void>;
91
+ /** EndpointManagement.muteGroupMember —— GROUP_MUTE(duration 秒,默认 600)。 */
92
+ muteMember(groupId: number | string, userId: number | string, duration?: number): Promise<void>;
93
+ banMember(groupId: number | string, userId: number | string, duration?: number): Promise<void>;
94
+ setGroupMute(groupId: number | string, userId: number | string, duration?: number): Promise<void>;
95
+ /** EndpointManagement.setGroupAdmin —— SET_GROUP_ADMIN(enable 默认 true)。 */
96
+ setModerator(groupId: number | string, userId: number | string, enable?: boolean): Promise<void>;
97
+ setAdmin(groupId: number | string, userId: number | string, enable?: boolean): Promise<void>;
98
+ setGroupAdmin(groupId: number | string, userId: number | string, enable?: boolean): Promise<void>;
49
99
  /** Test / internal: admit when open. */
50
100
  admit(msg: IcqqInboundMessage): void;
51
101
  }
package/lib/endpoint.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { formatCompact, getLogger } from '@zhin.js/logger';
2
2
  import { registerIcqqAgentEndpoint } from './icqq-agent-deps.js';
3
3
  import { InboundMessageDeduper, findIcqqNestedMessageSource, isIcqqBotMentioned, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, shouldSkipSelfInboundMessage, unwrapIcqqIpcEventPayload, } from './icqq-inbound.js';
4
- import { isIcqqGuildIpcEvent, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
4
+ import { buildIcqqInboxNoticeRow, buildIcqqInboxRequestRow, buildIcqqSystemRequestRow, isIcqqNoticePayload, isIcqqRequestPayload, } from './icqq-inbox.js';
5
+ import { IcqqGuildCatalog, isIcqqGuildIpcEvent, normalizeIcqqGuildInboundMessage, } from './icqq-guild.js';
5
6
  import { IpcClient } from './ipc-client.js';
6
7
  import { materializeOutboundBase64, resolveIcqqOutboundMediaMode, } from './outbound-media.js';
7
8
  import { Actions, formatInboundContent, formatInboundTarget, formatOutboundBody, parseSendTarget, } from './protocol.js';
@@ -14,10 +15,28 @@ export class IcqqIpcEndpoint {
14
15
  ipc;
15
16
  friends = new Map();
16
17
  groups = new Map();
18
+ #guildCatalog = new IcqqGuildCatalog();
19
+ management = Object.freeze({
20
+ listFriends: () => this.getFriendList(),
21
+ listGroups: () => this.getGroupList(),
22
+ listChannels: async () => {
23
+ await this.#guildCatalog.syncAll(this.ipc);
24
+ return this.#guildCatalog.getGuildChannelList();
25
+ },
26
+ listGroupMembers: (groupId) => this.getGroupMemberList(groupId),
27
+ approveRequest: (requestId, remark) => this.approveRequest(requestId, remark),
28
+ rejectRequest: (requestId, reason) => this.rejectRequest(requestId, reason),
29
+ kickGroupMember: (groupId, userId) => this.removeMember(groupId, userId),
30
+ muteGroupMember: (groupId, userId, duration) => this.muteMember(groupId, userId, duration),
31
+ setGroupAdmin: (groupId, userId, enabled) => this.setModerator(groupId, userId, enabled),
32
+ deleteFriend: (userId) => this.deleteFriend(userId),
33
+ });
17
34
  #open = false;
18
35
  #started = false;
19
36
  #subscriptions = [];
20
37
  #inboundDeduper = new InboundMessageDeduper();
38
+ /** request/notice 去重(推送事件与 GET_SYSTEM_MSG 首拉可能重复同一 flag)。 */
39
+ #inboxDeduper = new InboundMessageDeduper();
21
40
  #unregisterAgent;
22
41
  /** 用户主动 stop 时为 true,阻止自动重连 */
23
42
  #intentionalDisconnect = false;
@@ -71,6 +90,9 @@ export class IcqqIpcEndpoint {
71
90
  this.#handleEvent(event);
72
91
  });
73
92
  this.#subscriptions.push(sub);
93
+ // daemon 推送 client.em 全部事件,request/notice 实时到达;
94
+ // 启动时补一次 GET_SYSTEM_MSG,捞离线期间积存的待处理请求。
95
+ void this.#pullPendingSystemMessages();
74
96
  }
75
97
  /** IPC/RPC 意外断开时调度重连(指数退避) */
76
98
  #scheduleReconnect() {
@@ -145,8 +167,10 @@ export class IcqqIpcEndpoint {
145
167
  this.#unregisterAgent?.();
146
168
  this.#unregisterAgent = undefined;
147
169
  this.#inboundDeduper.clear();
170
+ this.#inboxDeduper.clear();
148
171
  this.friends.clear();
149
172
  this.groups.clear();
173
+ this.#guildCatalog.clear();
150
174
  try {
151
175
  this.ipc?.close();
152
176
  }
@@ -342,6 +366,132 @@ export class IcqqIpcEndpoint {
342
366
  }));
343
367
  }
344
368
  }
369
+ // ── 社交/群管(Console endpoint RPC 探测面)────────────────────────
370
+ // console-rpc-extended.ts 按方法名探测这些接口;全部薄封装走 IPC daemon。
371
+ /** EndpointManagement.listFriends —— 好友列表(LIST_FRIENDS,归一为 {user_id, nickname, remark})。 */
372
+ async getFriendList() {
373
+ const resp = await this.#mustRequest(Actions.LIST_FRIENDS, undefined, '获取好友列表');
374
+ const list = Array.isArray(resp.data) ? resp.data : [];
375
+ return list.map((f) => ({
376
+ user_id: f.user_id,
377
+ nickname: f.nickname,
378
+ remark: f.remark ?? '',
379
+ }));
380
+ }
381
+ /** EndpointManagement.listGroups —— 群列表(LIST_GROUPS,归一为 {group_id, name})。 */
382
+ async getGroupList() {
383
+ const resp = await this.#mustRequest(Actions.LIST_GROUPS, undefined, '获取群列表');
384
+ const list = Array.isArray(resp.data) ? resp.data : [];
385
+ return list.map((g) => ({
386
+ group_id: g.group_id,
387
+ name: g.group_name,
388
+ }));
389
+ }
390
+ /** EndpointManagement.listGroupMembers —— 群成员列表(字段对齐 daemon 返回)。 */
391
+ async getGroupMemberList(groupId) {
392
+ const resp = await this.#mustRequest(Actions.LIST_GROUP_MEMBERS, { group_id: toNumericId(groupId, 'group_id') }, '获取群成员列表');
393
+ return Array.isArray(resp.data) ? resp.data : [];
394
+ }
395
+ /** getGroupMemberList 别名(console 探测 listMembers / getMemberList)。 */
396
+ listMembers(groupId) {
397
+ return this.getGroupMemberList(groupId);
398
+ }
399
+ getMemberList(groupId) {
400
+ return this.getGroupMemberList(groupId);
401
+ }
402
+ /**
403
+ * EndpointManagement.resolveRequest —— id 为 inbox 行的 platform_request_id。
404
+ * 先 GET_SYSTEM_MSG 按 flag(回退 seq)定位请求,再按类型路由 handle_friend/group_request。
405
+ */
406
+ async approveRequest(id, remark) {
407
+ await this.#handleSystemRequest(id, true, { remark });
408
+ }
409
+ async rejectRequest(id, reason) {
410
+ await this.#handleSystemRequest(id, false, { reason });
411
+ }
412
+ /** EndpointManagement.deleteFriend —— FRIEND_DELETE。 */
413
+ async deleteFriend(userId) {
414
+ await this.#mustRequest(Actions.FRIEND_DELETE, { user_id: toNumericId(userId, 'user_id') }, '删除好友');
415
+ }
416
+ /** deleteFriend 别名(console 探测 delete_friend)。 */
417
+ delete_friend(userId) {
418
+ return this.deleteFriend(userId);
419
+ }
420
+ /** EndpointManagement.kickGroupMember —— GROUP_KICK。 */
421
+ async removeMember(groupId, userId) {
422
+ await this.#mustRequest(Actions.GROUP_KICK, { group_id: toNumericId(groupId, 'group_id'), user_id: toNumericId(userId, 'user_id') }, '踢出群成员');
423
+ }
424
+ kickMember(groupId, userId) {
425
+ return this.removeMember(groupId, userId);
426
+ }
427
+ setGroupKick(groupId, userId) {
428
+ return this.removeMember(groupId, userId);
429
+ }
430
+ /** EndpointManagement.muteGroupMember —— GROUP_MUTE(duration 秒,默认 600)。 */
431
+ async muteMember(groupId, userId, duration = 600) {
432
+ await this.#mustRequest(Actions.GROUP_MUTE, {
433
+ group_id: toNumericId(groupId, 'group_id'),
434
+ user_id: toNumericId(userId, 'user_id'),
435
+ duration,
436
+ }, '禁言群成员');
437
+ }
438
+ banMember(groupId, userId, duration = 600) {
439
+ return this.muteMember(groupId, userId, duration);
440
+ }
441
+ setGroupMute(groupId, userId, duration = 600) {
442
+ return this.muteMember(groupId, userId, duration);
443
+ }
444
+ /** EndpointManagement.setGroupAdmin —— SET_GROUP_ADMIN(enable 默认 true)。 */
445
+ async setModerator(groupId, userId, enable = true) {
446
+ await this.#mustRequest(Actions.SET_GROUP_ADMIN, {
447
+ group_id: toNumericId(groupId, 'group_id'),
448
+ user_id: toNumericId(userId, 'user_id'),
449
+ enable,
450
+ }, '设置群管理员');
451
+ }
452
+ setAdmin(groupId, userId, enable = true) {
453
+ return this.setModerator(groupId, userId, enable);
454
+ }
455
+ setGroupAdmin(groupId, userId, enable = true) {
456
+ return this.setModerator(groupId, userId, enable);
457
+ }
458
+ /** IPC 请求并统一错误上下文:daemon 返回 ok=false 时抛出带操作名的错误。 */
459
+ async #mustRequest(action, params, label) {
460
+ const resp = await this.request(action, params);
461
+ if (!resp.ok) {
462
+ throw new Error(`icqq ${label}失败: ${resp.error ?? 'daemon 未返回错误详情'}`);
463
+ }
464
+ return resp;
465
+ }
466
+ /** 按 flag/seq 在 GET_SYSTEM_MSG 中定位请求,路由到好友/群请求处理 action。 */
467
+ async #handleSystemRequest(id, approve, extra) {
468
+ const resp = await this.#mustRequest(Actions.GET_SYSTEM_MSG, undefined, '获取待处理请求');
469
+ const data = resp.data;
470
+ const friendRequests = Array.isArray(data?.friendRequests)
471
+ ? data.friendRequests
472
+ : [];
473
+ const groupRequests = Array.isArray(data?.groupRequests)
474
+ ? data.groupRequests
475
+ : [];
476
+ const matches = (m) => m.flag === id || (m.seq != null && String(m.seq) === id);
477
+ const friend = friendRequests.find(matches);
478
+ const group = friend ? undefined : groupRequests.find(matches);
479
+ const target = friend ?? group;
480
+ if (!target?.flag) {
481
+ throw new Error(`icqq 未找到待处理请求: ${id}(GET_SYSTEM_MSG 中无匹配 flag/seq)`);
482
+ }
483
+ const params = { flag: target.flag, approve };
484
+ if (friend) {
485
+ if (extra.remark)
486
+ params.remark = extra.remark;
487
+ await this.#mustRequest(Actions.HANDLE_FRIEND_REQUEST, params, '处理好友请求');
488
+ }
489
+ else {
490
+ if (extra.reason)
491
+ params.reason = extra.reason;
492
+ await this.#mustRequest(Actions.HANDLE_GROUP_REQUEST, params, '处理群请求');
493
+ }
494
+ }
345
495
  /** Test / internal: admit when open. */
346
496
  admit(msg) {
347
497
  if (!this.#open)
@@ -394,6 +544,14 @@ export class IcqqIpcEndpoint {
394
544
  const payload = unwrapIcqqIpcEventPayload(event);
395
545
  if (!payload || typeof payload !== 'object')
396
546
  return;
547
+ if (isIcqqRequestPayload(payload)) {
548
+ this.#recordInboxRequest(payload);
549
+ return;
550
+ }
551
+ if (isIcqqNoticePayload(payload)) {
552
+ this.#recordInboxNotice(payload);
553
+ return;
554
+ }
397
555
  if (!isIcqqMessagePostType(payload))
398
556
  return;
399
557
  const data = payload;
@@ -427,6 +585,7 @@ export class IcqqIpcEndpoint {
427
585
  const normalized = normalizeIcqqGuildInboundMessage(payload);
428
586
  if (!normalized)
429
587
  return;
588
+ this.#guildCatalog.upsertFromInbound(normalized.raw);
430
589
  if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`))
431
590
  return;
432
591
  this.admit({
@@ -445,6 +604,71 @@ export class IcqqIpcEndpoint {
445
604
  },
446
605
  });
447
606
  }
607
+ /** 收件箱行公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
608
+ #inboxBase() {
609
+ const id = String(this.#options.id);
610
+ return { adapter: id.split('\0').pop() ?? id, endpointId: this.name };
611
+ }
612
+ #recordInboxRequest(payload) {
613
+ const hooks = this.#options.inbox;
614
+ if (!hooks)
615
+ return;
616
+ const row = buildIcqqInboxRequestRow(payload, this.#inboxBase());
617
+ if (!row)
618
+ return;
619
+ if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`))
620
+ return;
621
+ void hooks.recordRequest(row);
622
+ hooks.publish?.('endpoint:request', row);
623
+ }
624
+ #recordInboxNotice(payload) {
625
+ const hooks = this.#options.inbox;
626
+ if (!hooks)
627
+ return;
628
+ const row = buildIcqqInboxNoticeRow(payload, this.#inboxBase());
629
+ if (!row)
630
+ return;
631
+ if (!this.#inboxDeduper.shouldProcess(`notice:${String(row.platform_notice_id)}`))
632
+ return;
633
+ void hooks.recordNotice(row);
634
+ hooks.publish?.('endpoint:notice', row);
635
+ }
636
+ /** 启动/重连后首拉 GET_SYSTEM_MSG:补录离线期间的好友/群待处理请求。 */
637
+ async #pullPendingSystemMessages() {
638
+ const hooks = this.#options.inbox;
639
+ if (!hooks || !this.ipc)
640
+ return;
641
+ try {
642
+ const resp = await this.ipc.request(Actions.GET_SYSTEM_MSG);
643
+ if (!resp.ok)
644
+ return;
645
+ const data = resp.data;
646
+ const base = this.#inboxBase();
647
+ const rows = [
648
+ ...(Array.isArray(data?.friendRequests)
649
+ ? data.friendRequests.map((m) => buildIcqqSystemRequestRow(m, 'friend', base))
650
+ : []),
651
+ ...(Array.isArray(data?.groupRequests)
652
+ ? data.groupRequests.map((m) => buildIcqqSystemRequestRow(m, 'group', base))
653
+ : []),
654
+ ];
655
+ for (const row of rows) {
656
+ if (!row)
657
+ continue;
658
+ if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`))
659
+ continue;
660
+ void hooks.recordRequest(row);
661
+ hooks.publish?.('endpoint:request', row);
662
+ }
663
+ }
664
+ catch (error) {
665
+ logger.debug(formatCompact({
666
+ op: 'inbox_pull_system_msg',
667
+ endpoint: this.name,
668
+ error: error instanceof Error ? error.message : String(error),
669
+ }));
670
+ }
671
+ }
448
672
  }
449
673
  /**
450
674
  * 组装入站 metadata:在基础字段上补充 quote 链路信息。
@@ -474,6 +698,14 @@ function buildIcqqQuoteMetadata(data, base) {
474
698
  metadata.quote_content = quoteText;
475
699
  return metadata;
476
700
  }
701
+ /** console RPC 传入的 gid/uid 可能是字符串,统一收敛为数字。 */
702
+ function toNumericId(value, label) {
703
+ const n = Number(value);
704
+ if (!Number.isFinite(n) || String(value).trim() === '') {
705
+ throw new TypeError(`icqq ${label} 必须是数字: ${String(value)}`);
706
+ }
707
+ return n;
708
+ }
477
709
  async function defaultCreateIpc(config) {
478
710
  const uin = Number(config.name);
479
711
  if (config.rpc) {
@@ -3,7 +3,13 @@
3
3
  */
4
4
  import { type MessageSegment } from "zhin.js";
5
5
  import { type IpcGuildMessageEventData } from "./protocol.js";
6
- import type { IpcClient } from "./ipc-client.js";
6
+ /** Narrow request port shared by the real IPC client and endpoint test transports. */
7
+ export interface IcqqGuildRpc {
8
+ request(action: string, params?: Record<string, unknown>): Promise<{
9
+ ok: boolean;
10
+ data?: unknown;
11
+ }>;
12
+ }
7
13
  export interface IcqqGuildChannelEntry {
8
14
  channel_id: string;
9
15
  channel_name: string;
@@ -27,7 +33,7 @@ export declare class IcqqGuildCatalog {
27
33
  private readonly channels;
28
34
  private readonly guilds;
29
35
  clear(): void;
30
- syncAll(ipc: IpcClient): Promise<void>;
36
+ syncAll(ipc: IcqqGuildRpc): Promise<void>;
31
37
  upsertFromInbound(data: IpcGuildMessageEventData): void;
32
38
  getGuildChannelList(): Array<{
33
39
  id: string;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * icqq IPC request/notice 事件 → 统一收件箱行(unified_inbox_request / unified_inbox_notice)。
3
+ *
4
+ * daemon 经 IpcEvent 推送 icqq client.em 分发的全部事件(见 protocol.ts),
5
+ * 好友/群请求(request.friend.add、request.group.add、request.group.invite)
6
+ * 与通知(notice.friend.*、notice.group.*)随事件流实时到达,无需轮询;
7
+ * endpoint 启动时补一次 GET_SYSTEM_MSG 拉取离线期间积存的待处理请求。
8
+ */
9
+ import type { IpcSystemMessage } from './types.js';
10
+ /** 行级公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
11
+ export interface IcqqInboxBase {
12
+ readonly adapter: string;
13
+ readonly endpointId: string;
14
+ }
15
+ /** post_type=request / request.* 判定(OneBot 事件壳)。 */
16
+ export declare function isIcqqRequestPayload(payload: unknown): boolean;
17
+ /** post_type=notice / notice.* 判定。 */
18
+ export declare function isIcqqNoticePayload(payload: unknown): boolean;
19
+ /**
20
+ * request 事件载荷 → unified_inbox_request 行。
21
+ * platform_request_id 取 flag(审批回执句柄),缺省回退 seq;两者皆无则无法审批,丢弃。
22
+ */
23
+ export declare function buildIcqqInboxRequestRow(payload: Record<string, unknown>, base: IcqqInboxBase): Record<string, unknown> | null;
24
+ /**
25
+ * notice 事件载荷 → unified_inbox_notice 行。
26
+ * 事件本身无稳定 id,platform_notice_id 由 time + actor + type 合成(配合去重)。
27
+ */
28
+ export declare function buildIcqqInboxNoticeRow(payload: Record<string, unknown>, base: IcqqInboxBase): Record<string, unknown> | null;
29
+ /**
30
+ * GET_SYSTEM_MSG 返回项 → unified_inbox_request 行(启动时首次拉取)。
31
+ * friend 列表项 type 语义即好友请求;group 列表项 type 为 add/invite(记 sub_type)。
32
+ */
33
+ export declare function buildIcqqSystemRequestRow(message: IpcSystemMessage, kind: 'friend' | 'group', base: IcqqInboxBase): Record<string, unknown> | null;