@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 +25 -0
- package/README.md +23 -5
- package/adapters/icqq.ts +64 -2
- package/lib/endpoint.d.ts +52 -2
- package/lib/endpoint.js +233 -1
- package/lib/icqq-guild.d.ts +8 -2
- package/lib/icqq-inbox.d.ts +33 -0
- package/lib/icqq-inbox.js +129 -0
- package/lib/index.d.ts +1 -1
- package/lib/types.d.ts +15 -0
- package/package.json +11 -11
- package/schema.json +86 -17
- package/src/endpoint.ts +300 -2
- package/src/icqq-guild.ts +9 -2
- package/src/icqq-inbox.ts +155 -0
- package/src/index.ts +1 -0
- package/src/types.ts +16 -0
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,6 +447,181 @@ 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
627
|
if (!this.#open) return;
|
|
@@ -456,6 +674,14 @@ export class IcqqIpcEndpoint implements EndpointInstance {
|
|
|
456
674
|
}
|
|
457
675
|
const payload = unwrapIcqqIpcEventPayload(event);
|
|
458
676
|
if (!payload || typeof payload !== 'object') return;
|
|
677
|
+
if (isIcqqRequestPayload(payload)) {
|
|
678
|
+
this.#recordInboxRequest(payload as Record<string, unknown>);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (isIcqqNoticePayload(payload)) {
|
|
682
|
+
this.#recordInboxNotice(payload as Record<string, unknown>);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
459
685
|
if (!isIcqqMessagePostType(payload)) return;
|
|
460
686
|
const data = payload as IcqqIpcMessageEvent;
|
|
461
687
|
if (shouldSkipSelfInboundMessage(data)) return;
|
|
@@ -486,6 +712,7 @@ export class IcqqIpcEndpoint implements EndpointInstance {
|
|
|
486
712
|
payload as Parameters<typeof normalizeIcqqGuildInboundMessage>[0],
|
|
487
713
|
);
|
|
488
714
|
if (!normalized) return;
|
|
715
|
+
this.#guildCatalog.upsertFromInbound(normalized.raw);
|
|
489
716
|
if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) return;
|
|
490
717
|
this.admit({
|
|
491
718
|
id: normalized.messageId,
|
|
@@ -503,6 +730,68 @@ export class IcqqIpcEndpoint implements EndpointInstance {
|
|
|
503
730
|
},
|
|
504
731
|
});
|
|
505
732
|
}
|
|
733
|
+
|
|
734
|
+
/** 收件箱行公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
|
|
735
|
+
#inboxBase(): { adapter: string; endpointId: string } {
|
|
736
|
+
const id = String(this.#options.id);
|
|
737
|
+
return { adapter: id.split('\0').pop() ?? id, endpointId: this.name };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
#recordInboxRequest(payload: Record<string, unknown>): void {
|
|
741
|
+
const hooks = this.#options.inbox;
|
|
742
|
+
if (!hooks) return;
|
|
743
|
+
const row = buildIcqqInboxRequestRow(payload, this.#inboxBase());
|
|
744
|
+
if (!row) return;
|
|
745
|
+
if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`)) return;
|
|
746
|
+
void hooks.recordRequest(row);
|
|
747
|
+
hooks.publish?.('endpoint:request', row);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
#recordInboxNotice(payload: Record<string, unknown>): void {
|
|
751
|
+
const hooks = this.#options.inbox;
|
|
752
|
+
if (!hooks) return;
|
|
753
|
+
const row = buildIcqqInboxNoticeRow(payload, this.#inboxBase());
|
|
754
|
+
if (!row) return;
|
|
755
|
+
if (!this.#inboxDeduper.shouldProcess(`notice:${String(row.platform_notice_id)}`)) return;
|
|
756
|
+
void hooks.recordNotice(row);
|
|
757
|
+
hooks.publish?.('endpoint:notice', row);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/** 启动/重连后首拉 GET_SYSTEM_MSG:补录离线期间的好友/群待处理请求。 */
|
|
761
|
+
async #pullPendingSystemMessages(): Promise<void> {
|
|
762
|
+
const hooks = this.#options.inbox;
|
|
763
|
+
if (!hooks || !this.ipc) return;
|
|
764
|
+
try {
|
|
765
|
+
const resp = await this.ipc.request(Actions.GET_SYSTEM_MSG);
|
|
766
|
+
if (!resp.ok) return;
|
|
767
|
+
const data = resp.data as
|
|
768
|
+
| { friendRequests?: unknown; groupRequests?: unknown }
|
|
769
|
+
| undefined;
|
|
770
|
+
const base = this.#inboxBase();
|
|
771
|
+
const rows = [
|
|
772
|
+
...(Array.isArray(data?.friendRequests)
|
|
773
|
+
? (data.friendRequests as IpcSystemMessage[]).map((m) =>
|
|
774
|
+
buildIcqqSystemRequestRow(m, 'friend', base))
|
|
775
|
+
: []),
|
|
776
|
+
...(Array.isArray(data?.groupRequests)
|
|
777
|
+
? (data.groupRequests as IpcSystemMessage[]).map((m) =>
|
|
778
|
+
buildIcqqSystemRequestRow(m, 'group', base))
|
|
779
|
+
: []),
|
|
780
|
+
];
|
|
781
|
+
for (const row of rows) {
|
|
782
|
+
if (!row) continue;
|
|
783
|
+
if (!this.#inboxDeduper.shouldProcess(`request:${String(row.platform_request_id)}`)) continue;
|
|
784
|
+
void hooks.recordRequest(row);
|
|
785
|
+
hooks.publish?.('endpoint:request', row);
|
|
786
|
+
}
|
|
787
|
+
} catch (error) {
|
|
788
|
+
logger.debug(formatCompact({
|
|
789
|
+
op: 'inbox_pull_system_msg',
|
|
790
|
+
endpoint: this.name,
|
|
791
|
+
error: error instanceof Error ? error.message : String(error),
|
|
792
|
+
}));
|
|
793
|
+
}
|
|
794
|
+
}
|
|
506
795
|
}
|
|
507
796
|
|
|
508
797
|
/**
|
|
@@ -536,6 +825,15 @@ function buildIcqqQuoteMetadata(
|
|
|
536
825
|
return metadata;
|
|
537
826
|
}
|
|
538
827
|
|
|
828
|
+
/** console RPC 传入的 gid/uid 可能是字符串,统一收敛为数字。 */
|
|
829
|
+
function toNumericId(value: number | string, label: string): number {
|
|
830
|
+
const n = Number(value);
|
|
831
|
+
if (!Number.isFinite(n) || String(value).trim() === '') {
|
|
832
|
+
throw new TypeError(`icqq ${label} 必须是数字: ${String(value)}`);
|
|
833
|
+
}
|
|
834
|
+
return n;
|
|
835
|
+
}
|
|
836
|
+
|
|
539
837
|
async function defaultCreateIpc(config: ResolvedIcqqConfig): Promise<IcqqIpcTransport> {
|
|
540
838
|
const uin = Number(config.name);
|
|
541
839
|
if (config.rpc) {
|
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:
|
|
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
|
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
|
|
11
|
+
/** 行级公共前缀:adapter 槽 localName + endpoint live 名(uin)。 */
|
|
12
|
+
export interface IcqqInboxBase {
|
|
13
|
+
readonly adapter: string;
|
|
14
|
+
readonly endpointId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** post_type=request / request.* 判定(OneBot 事件壳)。 */
|
|
18
|
+
export function isIcqqRequestPayload(payload: unknown): boolean {
|
|
19
|
+
const postType = (payload as { post_type?: unknown } | null)?.post_type;
|
|
20
|
+
return typeof postType === 'string'
|
|
21
|
+
&& (postType === 'request' || postType.startsWith('request.'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** post_type=notice / notice.* 判定。 */
|
|
25
|
+
export function isIcqqNoticePayload(payload: unknown): boolean {
|
|
26
|
+
const postType = (payload as { post_type?: unknown } | null)?.post_type;
|
|
27
|
+
return typeof postType === 'string'
|
|
28
|
+
&& (postType === 'notice' || postType.startsWith('notice.'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* request 事件载荷 → unified_inbox_request 行。
|
|
33
|
+
* platform_request_id 取 flag(审批回执句柄),缺省回退 seq;两者皆无则无法审批,丢弃。
|
|
34
|
+
*/
|
|
35
|
+
export function buildIcqqInboxRequestRow(
|
|
36
|
+
payload: Record<string, unknown>,
|
|
37
|
+
base: IcqqInboxBase,
|
|
38
|
+
): Record<string, unknown> | null {
|
|
39
|
+
const userId = payload.user_id ?? payload.actor_id;
|
|
40
|
+
if (userId == null) return null;
|
|
41
|
+
const flag = asNonEmptyString(payload.flag);
|
|
42
|
+
const platformRequestId = flag ?? (payload.seq != null ? String(payload.seq) : '');
|
|
43
|
+
if (!platformRequestId) return null;
|
|
44
|
+
const requestType = asNonEmptyString(payload.request_type) ?? asNonEmptyString(payload.type);
|
|
45
|
+
const groupId = payload.group_id;
|
|
46
|
+
const isGroup = requestType === 'group' || groupId != null;
|
|
47
|
+
return {
|
|
48
|
+
adapter: base.adapter,
|
|
49
|
+
endpoint_id: base.endpointId,
|
|
50
|
+
platform_request_id: platformRequestId,
|
|
51
|
+
type: isGroup ? 'group' : (requestType ?? 'friend'),
|
|
52
|
+
scene_type: isGroup ? 'group' : null,
|
|
53
|
+
scene_id: String(groupId ?? userId),
|
|
54
|
+
sub_type: asNonEmptyString(payload.sub_type),
|
|
55
|
+
actor_id: String(userId),
|
|
56
|
+
actor_name: asNonEmptyString(payload.nickname),
|
|
57
|
+
comment: asNonEmptyString(payload.comment),
|
|
58
|
+
created_at: toMillis(payload.time),
|
|
59
|
+
resolved: 0,
|
|
60
|
+
resolved_at: null,
|
|
61
|
+
consumed: 0,
|
|
62
|
+
consumed_at: null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* notice 事件载荷 → unified_inbox_notice 行。
|
|
68
|
+
* 事件本身无稳定 id,platform_notice_id 由 time + actor + type 合成(配合去重)。
|
|
69
|
+
*/
|
|
70
|
+
export function buildIcqqInboxNoticeRow(
|
|
71
|
+
payload: Record<string, unknown>,
|
|
72
|
+
base: IcqqInboxBase,
|
|
73
|
+
): Record<string, unknown> | null {
|
|
74
|
+
const noticeType = asNonEmptyString(payload.notice_type) ?? asNonEmptyString(payload.type);
|
|
75
|
+
if (!noticeType) return null;
|
|
76
|
+
const subType = asNonEmptyString(payload.sub_type);
|
|
77
|
+
const userId = payload.user_id;
|
|
78
|
+
const operatorId = payload.operator_id;
|
|
79
|
+
const groupId = payload.group_id;
|
|
80
|
+
const createdAt = toMillis(payload.time);
|
|
81
|
+
const actorId = operatorId ?? userId;
|
|
82
|
+
const platformNoticeId = asNonEmptyString(payload.notice_id)
|
|
83
|
+
?? (payload.seq != null ? String(payload.seq) : undefined)
|
|
84
|
+
?? `${createdAt}_${String(actorId ?? '')}_${noticeType}${subType ? `.${subType}` : ''}`;
|
|
85
|
+
const targetId = userId != null && String(userId) !== String(actorId ?? '') ? userId : undefined;
|
|
86
|
+
return {
|
|
87
|
+
adapter: base.adapter,
|
|
88
|
+
endpoint_id: base.endpointId,
|
|
89
|
+
platform_notice_id: platformNoticeId,
|
|
90
|
+
type: noticeType,
|
|
91
|
+
scene_type: groupId != null ? 'group' : null,
|
|
92
|
+
scene_id: String(groupId ?? userId ?? operatorId ?? ''),
|
|
93
|
+
sub_type: subType,
|
|
94
|
+
actor_id: actorId != null ? String(actorId) : null,
|
|
95
|
+
actor_name: asNonEmptyString(payload.nickname),
|
|
96
|
+
target_id: targetId != null ? String(targetId) : null,
|
|
97
|
+
target_name: null,
|
|
98
|
+
payload: safeJson(payload),
|
|
99
|
+
created_at: createdAt,
|
|
100
|
+
consumed: 0,
|
|
101
|
+
consumed_at: null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* GET_SYSTEM_MSG 返回项 → unified_inbox_request 行(启动时首次拉取)。
|
|
107
|
+
* friend 列表项 type 语义即好友请求;group 列表项 type 为 add/invite(记 sub_type)。
|
|
108
|
+
*/
|
|
109
|
+
export function buildIcqqSystemRequestRow(
|
|
110
|
+
message: IpcSystemMessage,
|
|
111
|
+
kind: 'friend' | 'group',
|
|
112
|
+
base: IcqqInboxBase,
|
|
113
|
+
): Record<string, unknown> | null {
|
|
114
|
+
if (message.user_id == null) return null;
|
|
115
|
+
const platformRequestId = asNonEmptyString(message.flag)
|
|
116
|
+
?? (message.seq != null ? String(message.seq) : '');
|
|
117
|
+
if (!platformRequestId) return null;
|
|
118
|
+
return {
|
|
119
|
+
adapter: base.adapter,
|
|
120
|
+
endpoint_id: base.endpointId,
|
|
121
|
+
platform_request_id: platformRequestId,
|
|
122
|
+
type: kind,
|
|
123
|
+
scene_type: kind === 'group' ? 'group' : null,
|
|
124
|
+
scene_id: String(kind === 'group' ? (message.group_id ?? message.user_id) : message.user_id),
|
|
125
|
+
sub_type: kind === 'group' ? (asNonEmptyString(message.type) ?? null) : null,
|
|
126
|
+
actor_id: String(message.user_id),
|
|
127
|
+
actor_name: asNonEmptyString(message.nickname),
|
|
128
|
+
comment: asNonEmptyString(message.comment),
|
|
129
|
+
created_at: toMillis(message.time),
|
|
130
|
+
resolved: 0,
|
|
131
|
+
resolved_at: null,
|
|
132
|
+
consumed: 0,
|
|
133
|
+
consumed_at: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function asNonEmptyString(value: unknown): string | null {
|
|
138
|
+
if (typeof value !== 'string') return null;
|
|
139
|
+
const trimmed = value.trim();
|
|
140
|
+
return trimmed ? trimmed : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** oicq 事件 time 为秒;缺失时回退当前时间。 */
|
|
144
|
+
function toMillis(time: unknown): number {
|
|
145
|
+
const seconds = Number(time);
|
|
146
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : Date.now();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function safeJson(value: unknown): string {
|
|
150
|
+
try {
|
|
151
|
+
return JSON.stringify(value ?? {});
|
|
152
|
+
} catch {
|
|
153
|
+
return '{}';
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -76,3 +76,19 @@ export interface IpcMemberInfo {
|
|
|
76
76
|
last_sent_time?: number;
|
|
77
77
|
shutup_time?: number;
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* GET_SYSTEM_MSG 归一后的待处理请求(@icqqjs/cli NormalizedSystemMessage)。
|
|
82
|
+
* friendRequests 与 groupRequests 均为该形状,按 flag 处理。
|
|
83
|
+
*/
|
|
84
|
+
export interface IpcSystemMessage {
|
|
85
|
+
type: string;
|
|
86
|
+
user_id?: number;
|
|
87
|
+
nickname?: string;
|
|
88
|
+
group_id?: number;
|
|
89
|
+
group_name?: string;
|
|
90
|
+
comment?: string;
|
|
91
|
+
flag?: string;
|
|
92
|
+
seq?: number;
|
|
93
|
+
time?: number;
|
|
94
|
+
}
|