@zhin.js/adapter-icqq 6.0.2 → 6.0.3
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 +40 -0
- package/README.md +32 -437
- package/adapters/icqq.ts +30 -0
- package/agent/tools/announce.ts +2 -2
- package/agent/tools/essence.ts +2 -2
- package/agent/tools/friend_list.ts +2 -2
- package/agent/tools/group_files.ts +2 -2
- package/agent/tools/group_invite.ts +2 -2
- package/agent/tools/group_list.ts +2 -2
- package/agent/tools/group_sign.ts +2 -2
- package/agent/tools/list_muted.ts +2 -2
- package/agent/tools/poke.ts +2 -2
- package/agent/tools/send_user_like.ts +2 -2
- package/agent/tools/set_anonymous.ts +2 -2
- package/agent/tools/set_title.ts +2 -2
- package/lib/agent-prompt.d.ts +2 -0
- package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
- package/lib/cq-message.d.ts +9 -0
- package/lib/{src/cq-message.js → cq-message.js} +0 -1
- package/lib/endpoint.d.ts +51 -0
- package/lib/endpoint.js +483 -0
- package/lib/forward-msg.d.ts +26 -0
- package/lib/{src/forward-msg.js → forward-msg.js} +0 -1
- package/lib/icqq-agent-deps.d.ts +25 -0
- package/lib/icqq-agent-deps.js +32 -0
- package/lib/icqq-guild.d.ts +47 -0
- package/lib/{src/icqq-guild.js → icqq-guild.js} +4 -2
- package/lib/icqq-inbound.d.ts +139 -0
- package/lib/{src/icqq-inbound.js → icqq-inbound.js} +27 -3
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/ipc-client.d.ts +73 -0
- package/lib/{src/ipc-client.js → ipc-client.js} +0 -1
- package/lib/outbound-media.d.ts +13 -0
- package/lib/{src/outbound-media.js → outbound-media.js} +0 -1
- package/lib/protocol.d.ts +269 -0
- package/lib/{src/protocol.js → protocol.js} +124 -2
- package/lib/types.d.ts +72 -0
- package/lib/{src/types.js → types.js} +0 -1
- package/package.json +49 -45
- package/plugin.ts +32 -0
- package/schema.json +35 -0
- package/src/endpoint.ts +407 -796
- package/src/icqq-agent-deps.ts +48 -9
- package/src/icqq-guild.ts +1 -2
- package/src/icqq-inbound.ts +31 -2
- package/src/index.ts +35 -69
- package/src/protocol.ts +191 -1
- package/client/ICQQManagement.tsx +0 -147
- package/client/LoginAssistPanel.tsx +0 -300
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -34
- package/lib/agent/tools/announce.js +0 -21
- package/lib/agent/tools/announce.js.map +0 -1
- package/lib/agent/tools/essence.js +0 -22
- package/lib/agent/tools/essence.js.map +0 -1
- package/lib/agent/tools/friend_list.js +0 -19
- package/lib/agent/tools/friend_list.js.map +0 -1
- package/lib/agent/tools/group_files.js +0 -29
- package/lib/agent/tools/group_files.js.map +0 -1
- package/lib/agent/tools/group_invite.js +0 -20
- package/lib/agent/tools/group_invite.js.map +0 -1
- package/lib/agent/tools/group_list.js +0 -20
- package/lib/agent/tools/group_list.js.map +0 -1
- package/lib/agent/tools/group_sign.js +0 -19
- package/lib/agent/tools/group_sign.js.map +0 -1
- package/lib/agent/tools/list_muted.js +0 -23
- package/lib/agent/tools/list_muted.js.map +0 -1
- package/lib/agent/tools/poke.js +0 -20
- package/lib/agent/tools/poke.js.map +0 -1
- package/lib/agent/tools/send_user_like.js +0 -22
- package/lib/agent/tools/send_user_like.js.map +0 -1
- package/lib/agent/tools/set_anonymous.js +0 -22
- package/lib/agent/tools/set_anonymous.js.map +0 -1
- package/lib/agent/tools/set_title.js +0 -25
- package/lib/agent/tools/set_title.js.map +0 -1
- package/lib/src/adapter.js +0 -128
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/agent-prompt.js.map +0 -1
- package/lib/src/cq-message.js.map +0 -1
- package/lib/src/endpoint.js +0 -755
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/forward-msg.js.map +0 -1
- package/lib/src/get-msg.js +0 -46
- package/lib/src/get-msg.js.map +0 -1
- package/lib/src/icqq-agent-deps.js +0 -10
- package/lib/src/icqq-agent-deps.js.map +0 -1
- package/lib/src/icqq-guild.js.map +0 -1
- package/lib/src/icqq-inbound.js.map +0 -1
- package/lib/src/icqq-login-assist-bridge.js +0 -148
- package/lib/src/icqq-login-assist-bridge.js.map +0 -1
- package/lib/src/icqq-side-events.js +0 -130
- package/lib/src/icqq-side-events.js.map +0 -1
- package/lib/src/index.js +0 -50
- package/lib/src/index.js.map +0 -1
- package/lib/src/ipc-client.js.map +0 -1
- package/lib/src/login-assist-routes.js +0 -29
- package/lib/src/login-assist-routes.js.map +0 -1
- package/lib/src/login-ipc-contract.js +0 -35
- package/lib/src/login-ipc-contract.js.map +0 -1
- package/lib/src/outbound-media.js.map +0 -1
- package/lib/src/protocol.js.map +0 -1
- package/lib/src/routes.js +0 -67
- package/lib/src/routes.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/tools/index.js +0 -23
- package/lib/src/tools/index.js.map +0 -1
- package/lib/src/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -151
- package/src/get-msg.ts +0 -56
- package/src/icqq-login-assist-bridge.ts +0 -196
- package/src/icqq-side-events.ts +0 -143
- package/src/login-assist-routes.ts +0 -39
- package/src/login-ipc-contract.ts +0 -52
- package/src/routes.ts +0 -83
- package/src/segment-mapper.ts +0 -1
- package/src/tools/index.ts +0 -31
package/src/endpoint.ts
CHANGED
|
@@ -1,846 +1,307 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* 不再直接依赖 @icqqjs/icqq 协议库。
|
|
5
|
-
* 登录由 `icqq login` 完成,本 Endpoint 只负责连接守护进程并收发消息。
|
|
2
|
+
* IcqqIpcEndpoint — lifecycle, outbound, IPC subscribe/admit for ICQQ daemon.
|
|
6
3
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
IpcGroupInfo,
|
|
13
|
-
IpcMemberInfo,
|
|
14
|
-
} from "./types.js";
|
|
15
|
-
import type { IcqqAdapter } from "./adapter.js";
|
|
16
|
-
import { IpcClient } from "./ipc-client.js";
|
|
17
|
-
import { Actions, type IpcEvent, type IpcGuildMessageEventData } from './protocol.js';
|
|
18
|
-
import { findIcqqNestedMessageSource, InboundMessageDeduper, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveQuoteIdFromIcqqSource, shouldSkipSelfInboundMessage, unwrapIcqqIpcEventPayload, type IcqqIpcMessageEvent, type NormalizedIcqqInbound } from './icqq-inbound.js';
|
|
19
|
-
|
|
4
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
5
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
6
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
8
|
+
import { registerIcqqAgentEndpoint } from './icqq-agent-deps.js';
|
|
20
9
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
InboundMessageDeduper,
|
|
11
|
+
findIcqqNestedMessageSource,
|
|
12
|
+
isIcqqBotMentioned,
|
|
13
|
+
isIcqqMessagePostType,
|
|
14
|
+
normalizeIcqqInboundMessage,
|
|
15
|
+
quotedPayloadFromIcqqSource,
|
|
16
|
+
resolveIcqqQuoteIdFromEvent,
|
|
17
|
+
shouldSkipSelfInboundMessage,
|
|
18
|
+
unwrapIcqqIpcEventPayload,
|
|
19
|
+
type IcqqIpcMessageEvent,
|
|
20
|
+
} from './icqq-inbound.js';
|
|
21
|
+
import {
|
|
22
|
+
isIcqqGuildIpcEvent,
|
|
23
|
+
normalizeIcqqGuildInboundMessage,
|
|
24
|
+
} from './icqq-guild.js';
|
|
25
|
+
import { IpcClient } from './ipc-client.js';
|
|
25
26
|
import {
|
|
26
27
|
materializeOutboundBase64,
|
|
27
28
|
resolveIcqqOutboundMediaMode,
|
|
28
|
-
} from
|
|
29
|
-
import {
|
|
30
|
-
formatIcqqMetaLog,
|
|
31
|
-
formatIcqqNotice,
|
|
32
|
-
formatIcqqRequest,
|
|
33
|
-
isIcqqMetaPayload,
|
|
34
|
-
isIcqqNoticePayload,
|
|
35
|
-
isIcqqRequestPayload,
|
|
36
|
-
resolveIcqqEventPostType,
|
|
37
|
-
shouldRefreshListsOnMeta,
|
|
38
|
-
type IcqqIpcRawEvent,
|
|
39
|
-
} from "./icqq-side-events.js";
|
|
29
|
+
} from './outbound-media.js';
|
|
40
30
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} from
|
|
31
|
+
Actions,
|
|
32
|
+
formatInboundContent,
|
|
33
|
+
formatInboundTarget,
|
|
34
|
+
formatOutboundBody,
|
|
35
|
+
parseSendTarget,
|
|
36
|
+
type IcqqInboundMessage,
|
|
37
|
+
type IpcEvent,
|
|
38
|
+
type IpcResponse,
|
|
39
|
+
type ResolvedIcqqConfig,
|
|
40
|
+
} from './protocol.js';
|
|
41
|
+
import type { IpcFriendInfo, IpcGroupInfo } from './types.js';
|
|
42
|
+
|
|
43
|
+
const logger = getLogger('icqq');
|
|
44
|
+
|
|
45
|
+
/** Minimal IPC surface used by the endpoint (real IpcClient or test mock). */
|
|
46
|
+
export interface IcqqIpcTransport {
|
|
47
|
+
request(action: string, params?: Record<string, unknown>): Promise<IpcResponse>;
|
|
48
|
+
subscribe(
|
|
49
|
+
action: string,
|
|
50
|
+
params: Record<string, unknown>,
|
|
51
|
+
handler: (event: IpcEvent) => void,
|
|
52
|
+
): { unsubscribe: () => Promise<void> };
|
|
53
|
+
setOnRemoteDisconnect(handler: (() => void) | null): void;
|
|
54
|
+
close(): void;
|
|
55
|
+
}
|
|
52
56
|
|
|
53
|
-
export
|
|
54
|
-
$connected = false;
|
|
55
|
-
$config: IcqqEndpointConfig;
|
|
56
|
-
ipc!: IpcClient;
|
|
57
|
+
export type CreateIcqqIpc = (config: ResolvedIcqqConfig) => Promise<IcqqIpcTransport>;
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
export interface IcqqEndpointOptions {
|
|
60
|
+
readonly id: CapabilityId;
|
|
61
|
+
readonly gateway: MessageGateway;
|
|
62
|
+
readonly config: ResolvedIcqqConfig;
|
|
63
|
+
readonly createIpc?: CreateIcqqIpc;
|
|
64
|
+
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
export class IcqqIpcEndpoint implements EndpointInstance {
|
|
67
|
+
readonly #options: IcqqEndpointOptions;
|
|
68
|
+
readonly #createIpc: CreateIcqqIpc;
|
|
69
|
+
readonly name: string;
|
|
70
|
+
/** Populated after start(); agent tools read this. */
|
|
71
|
+
ipc!: IcqqIpcTransport;
|
|
72
|
+
readonly friends = new Map<number, IpcFriendInfo>();
|
|
73
|
+
readonly groups = new Map<number, IpcGroupInfo>();
|
|
74
|
+
#open = false;
|
|
75
|
+
#started = false;
|
|
76
|
+
#subscriptions: Array<{ unsubscribe: () => Promise<void> }> = [];
|
|
77
|
+
#inboundDeduper = new InboundMessageDeduper();
|
|
78
|
+
#unregisterAgent?: () => void;
|
|
79
|
+
/** 用户主动 stop 时为 true,阻止自动重连 */
|
|
80
|
+
#intentionalDisconnect = false;
|
|
72
81
|
/** 是否已有重连循环在跑(避免多次 schedule 叠套) */
|
|
73
|
-
|
|
82
|
+
#reconnectRunning = false;
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
constructor(options: IcqqEndpointOptions) {
|
|
85
|
+
this.#options = options;
|
|
86
|
+
this.name = options.config.name;
|
|
87
|
+
this.#createIpc = options.createIpc ?? defaultCreateIpc;
|
|
77
88
|
}
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
public adapter: IcqqAdapter,
|
|
85
|
-
config: IcqqEndpointConfig,
|
|
86
|
-
) {
|
|
87
|
-
this.$config = config;
|
|
90
|
+
async request(action: string, params?: Record<string, unknown>): Promise<IpcResponse> {
|
|
91
|
+
if (!this.ipc) {
|
|
92
|
+
throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发起请求: ${action}`);
|
|
93
|
+
}
|
|
94
|
+
return this.ipc.request(action, params);
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
97
|
+
async start(): Promise<void> {
|
|
98
|
+
if (this.#started) return;
|
|
99
|
+
this.#started = true;
|
|
100
|
+
this.#intentionalDisconnect = false;
|
|
101
|
+
try {
|
|
102
|
+
await this.#bindIpcSession();
|
|
103
|
+
this.#unregisterAgent = registerIcqqAgentEndpoint(this.name, this);
|
|
104
|
+
logger.info(formatCompact({
|
|
105
|
+
op: 'connect',
|
|
106
|
+
endpoint: this.name,
|
|
107
|
+
mode: this.#options.config.rpc ? 'rpc' : 'ipc',
|
|
108
|
+
friends: this.friends.size,
|
|
109
|
+
groups: this.groups.size,
|
|
110
|
+
}));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
await this.stop();
|
|
113
|
+
// Startup connect failures are surfaced once (with stack) by AdapterIndex;
|
|
114
|
+
// keep this at debug to avoid a duplicate error-level log.
|
|
115
|
+
logger.debug(`Failed to connect ICQQ IPC: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
/** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
|
|
113
|
-
|
|
114
|
-
const uin = Number(this.$config.name);
|
|
115
|
-
const rpc = this.$config.rpc;
|
|
116
|
-
|
|
121
|
+
async #bindIpcSession(): Promise<void> {
|
|
117
122
|
// 旧连接上的订阅句柄已失效,不再对死 socket 发 unsubscribe
|
|
118
|
-
this
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
client.close();
|
|
129
|
-
throw new Error("连接已取消");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
this.ipc = client;
|
|
133
|
-
if (this.$config.autoReconnect !== false) {
|
|
134
|
-
this.ipc.setOnRemoteDisconnect(() => this.scheduleReconnect());
|
|
135
|
-
} else {
|
|
136
|
-
this.ipc.setOnRemoteDisconnect(null);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
await this.refreshLists();
|
|
140
|
-
|
|
141
|
-
// 新版 icqq cli 在认证后自动广播事件,订阅过滤改为客户端侧完成。
|
|
142
|
-
const sub = this.ipc.subscribe(
|
|
143
|
-
Actions.SUBSCRIBE,
|
|
144
|
-
{},
|
|
145
|
-
(event) => this.handleEvent(event),
|
|
146
|
-
);
|
|
147
|
-
this.subscriptions.push(sub);
|
|
148
|
-
|
|
149
|
-
this.$connected = true;
|
|
123
|
+
this.#subscriptions = [];
|
|
124
|
+
this.ipc = await this.#createIpc(this.#options.config);
|
|
125
|
+
if (this.#options.config.autoReconnect !== false) {
|
|
126
|
+
this.ipc.setOnRemoteDisconnect(() => this.#scheduleReconnect());
|
|
127
|
+
}
|
|
128
|
+
await this.#refreshLists();
|
|
129
|
+
const sub = this.ipc.subscribe(Actions.SUBSCRIBE, {}, (event) => {
|
|
130
|
+
this.#handleEvent(event);
|
|
131
|
+
});
|
|
132
|
+
this.#subscriptions.push(sub);
|
|
150
133
|
}
|
|
151
134
|
|
|
152
135
|
/** IPC/RPC 意外断开时调度重连(指数退避) */
|
|
153
|
-
|
|
154
|
-
if (this
|
|
155
|
-
if (this
|
|
156
|
-
if (this
|
|
157
|
-
this
|
|
158
|
-
void this
|
|
136
|
+
#scheduleReconnect(): void {
|
|
137
|
+
if (this.#options.config.autoReconnect === false) return;
|
|
138
|
+
if (this.#intentionalDisconnect) return;
|
|
139
|
+
if (this.#reconnectRunning) return;
|
|
140
|
+
this.#reconnectRunning = true;
|
|
141
|
+
void this.#runReconnectLoop();
|
|
159
142
|
}
|
|
160
143
|
|
|
161
|
-
|
|
144
|
+
async #runReconnectLoop(): Promise<void> {
|
|
162
145
|
try {
|
|
163
|
-
for (let attempt = 0; !this
|
|
146
|
+
for (let attempt = 0; !this.#intentionalDisconnect; attempt++) {
|
|
164
147
|
const base = Math.min(30_000, 500 * 2 ** Math.min(attempt, 6));
|
|
165
148
|
const jitter = Math.floor(Math.random() * 400);
|
|
166
149
|
const delayMs = base + jitter;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
op:
|
|
171
|
-
endpoint: this
|
|
150
|
+
// 首次断开 WARN,后续重试静默为 DEBUG,避免刷屏
|
|
151
|
+
const disconnectLog = attempt === 0 ? logger.warn.bind(logger) : logger.debug.bind(logger);
|
|
152
|
+
disconnectLog(formatCompact({
|
|
153
|
+
op: 'disconnect',
|
|
154
|
+
endpoint: this.name,
|
|
172
155
|
ok: false,
|
|
173
156
|
delay_ms: delayMs,
|
|
174
157
|
attempt: attempt + 1,
|
|
175
158
|
}));
|
|
176
|
-
|
|
177
159
|
await new Promise<void>((r) => setTimeout(r, delayMs));
|
|
178
|
-
if (this
|
|
179
|
-
|
|
160
|
+
if (this.#intentionalDisconnect) break;
|
|
180
161
|
try {
|
|
181
|
-
await this
|
|
182
|
-
|
|
183
|
-
op:
|
|
184
|
-
endpoint: this
|
|
162
|
+
await this.#bindIpcSession();
|
|
163
|
+
logger.info(formatCompact({
|
|
164
|
+
op: 'reconnect',
|
|
165
|
+
endpoint: this.name,
|
|
166
|
+
ok: true,
|
|
167
|
+
attempts: attempt + 1,
|
|
185
168
|
friends: this.friends.size,
|
|
186
169
|
groups: this.groups.size,
|
|
187
170
|
}));
|
|
188
171
|
break;
|
|
189
|
-
} catch (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const retryLog = attempt === 0 ? logger.warn.bind(logger) : logger.debug.bind(logger);
|
|
174
|
+
retryLog(formatCompact({
|
|
175
|
+
op: 'reconnect',
|
|
176
|
+
endpoint: this.name,
|
|
193
177
|
ok: false,
|
|
194
|
-
|
|
178
|
+
attempt: attempt + 1,
|
|
179
|
+
error: error instanceof Error ? error.message : String(error),
|
|
195
180
|
}));
|
|
196
181
|
}
|
|
197
182
|
}
|
|
198
183
|
} finally {
|
|
199
|
-
this
|
|
184
|
+
this.#reconnectRunning = false;
|
|
200
185
|
}
|
|
201
186
|
}
|
|
202
187
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const [flResp, glResp] = await Promise.all([
|
|
206
|
-
this.ipc.request(Actions.LIST_FRIENDS),
|
|
207
|
-
this.ipc.request(Actions.LIST_GROUPS),
|
|
208
|
-
]);
|
|
209
|
-
|
|
210
|
-
this.friends.clear();
|
|
211
|
-
if (flResp.ok && Array.isArray(flResp.data)) {
|
|
212
|
-
for (const f of flResp.data as IpcFriendInfo[]) {
|
|
213
|
-
this.friends.set(f.user_id, f);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
this.groups.clear();
|
|
218
|
-
if (glResp.ok && Array.isArray(glResp.data)) {
|
|
219
|
-
for (const g of glResp.data as IpcGroupInfo[]) {
|
|
220
|
-
this.groups.set(g.group_id, g);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
await this.guildCatalog.syncAll(this.ipc).catch((e: unknown) => {
|
|
225
|
-
this.logger.warn(formatCompact({
|
|
226
|
-
op: "sync_guilds",
|
|
227
|
-
endpoint: this.$id,
|
|
228
|
-
ok: false,
|
|
229
|
-
error: e instanceof Error ? e.message : String(e),
|
|
230
|
-
}));
|
|
231
|
-
});
|
|
188
|
+
open(): void {
|
|
189
|
+
this.#open = true;
|
|
232
190
|
}
|
|
233
191
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return this.guildCatalog.getGuildChannelList();
|
|
192
|
+
close(): void {
|
|
193
|
+
this.#open = false;
|
|
237
194
|
}
|
|
238
195
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
): { channelName?: string; parentName?: string } {
|
|
243
|
-
return this.guildCatalog.resolveConsoleChannelNames(channelId, guildId);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// ── 断开 ───────────────────────────────────────────────────────────
|
|
247
|
-
|
|
248
|
-
async $disconnect(): Promise<void> {
|
|
249
|
-
this.intentionalDisconnect = true;
|
|
250
|
-
|
|
196
|
+
async stop(): Promise<void> {
|
|
197
|
+
this.#intentionalDisconnect = true;
|
|
198
|
+
this.#open = false;
|
|
251
199
|
this.ipc?.setOnRemoteDisconnect(null);
|
|
252
|
-
for (const sub of this.
|
|
253
|
-
await sub.unsubscribe().catch(() => {});
|
|
254
|
-
}
|
|
255
|
-
this.subscriptions = [];
|
|
256
|
-
this.inboundDeduper.clear();
|
|
257
|
-
disposeIcqqLoginAssistBridge(this.$id);
|
|
258
|
-
this.ipc?.close();
|
|
259
|
-
this.$connected = false;
|
|
260
|
-
this.logger.debug(formatCompact( { op: "disconnect", endpoint: this.$id }));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ── 消息处理 ───────────────────────────────────────────────────────
|
|
264
|
-
|
|
265
|
-
private handleEvent(event: IpcEvent): void {
|
|
266
|
-
if (event.event?.startsWith("system.login.")) {
|
|
267
|
-
handleIcqqLoginIpcEvent(this, event.event, event.data);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (event.event === Actions.RELOAD_GUILDS) {
|
|
272
|
-
void this.guildCatalog.syncAll(this.ipc).catch((e: unknown) => {
|
|
273
|
-
this.logger.warn(formatCompact({
|
|
274
|
-
op: "reload_guilds",
|
|
275
|
-
endpoint: this.$id,
|
|
276
|
-
ok: false,
|
|
277
|
-
error: e instanceof Error ? e.message : String(e),
|
|
278
|
-
}));
|
|
279
|
-
});
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (isIcqqGuildIpcEvent(event.event)) {
|
|
284
|
-
this.handleGuildMessageEvent(event);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const payload = unwrapIcqqIpcEventPayload(event);
|
|
289
|
-
if (!payload || typeof payload !== "object") {
|
|
290
|
-
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
291
|
-
this.logger.info(
|
|
292
|
-
formatCompact({
|
|
293
|
-
ipc_skip: "no_payload",
|
|
294
|
-
ipc_event: event.event,
|
|
295
|
-
}),
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
302
|
-
this.logger.info(
|
|
303
|
-
formatCompact({
|
|
304
|
-
ipc_raw: JSON.stringify(payload).slice(0, 800),
|
|
305
|
-
}),
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (isIcqqNoticePayload(payload)) {
|
|
310
|
-
this.handleNoticeEvent(payload);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
if (isIcqqRequestPayload(payload)) {
|
|
314
|
-
this.handleRequestEvent(payload);
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
if (isIcqqMetaPayload(payload)) {
|
|
318
|
-
this.handleMetaEvent(payload);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (!isIcqqMessagePostType(payload)) {
|
|
323
|
-
const postType = resolveIcqqEventPostType(
|
|
324
|
-
payload as Record<string, unknown>,
|
|
325
|
-
);
|
|
326
|
-
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
327
|
-
this.logger.info(
|
|
328
|
-
formatCompact({ ipc_skip: postType ?? "unknown_event" }),
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const data = payload;
|
|
335
|
-
|
|
336
|
-
if (shouldSkipSelfInboundMessage(data)) {
|
|
337
|
-
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
338
|
-
this.logger.debug(formatCompact({ ipc_skip: "self_message" }));
|
|
339
|
-
}
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const normalized = normalizeIcqqInboundMessage(data);
|
|
344
|
-
if (!normalized) {
|
|
345
|
-
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
346
|
-
this.logger.debug(formatCompact({ ipc_skip: "normalize_failed" }));
|
|
347
|
-
}
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (!this.inboundDeduper.shouldProcess(normalized.messageId)) {
|
|
352
|
-
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
353
|
-
this.logger.info(
|
|
354
|
-
formatCompact({ ipc_dedupe: normalized.messageId }),
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
void this.dispatchInboundMessage(data, normalized);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
private async dispatchInboundMessage(
|
|
364
|
-
data: IcqqIpcMessageEvent,
|
|
365
|
-
normalized: NormalizedIcqqInbound,
|
|
366
|
-
): Promise<void> {
|
|
367
|
-
this.logIpcInboundPayload(data, normalized);
|
|
368
|
-
|
|
369
|
-
await this.primeQuotedSourceCache(
|
|
370
|
-
data.source ?? findIcqqNestedMessageSource(data),
|
|
371
|
-
);
|
|
372
|
-
|
|
373
|
-
const message = this.$formatMessage(normalized);
|
|
374
|
-
this.adapter.emit("message.receive", message);
|
|
375
|
-
this.logger.debug(
|
|
376
|
-
`${this.$id} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}${message.$quote_id ? ` quote_id=${message.$quote_id}` : ""}`,
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
private handleGuildMessageEvent(event: IpcEvent): void {
|
|
381
|
-
const payload = event.data;
|
|
382
|
-
if (!payload || typeof payload !== "object") return;
|
|
383
|
-
const data = payload as IpcGuildMessageEventData & Record<string, unknown>;
|
|
384
|
-
const normalized = normalizeIcqqGuildInboundMessage(data);
|
|
385
|
-
if (!normalized) return;
|
|
386
|
-
|
|
387
|
-
this.guildCatalog.upsertFromInbound(data);
|
|
388
|
-
|
|
389
|
-
if (!this.inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) {
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
void this.dispatchGuildInboundMessage(normalized);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
private async dispatchGuildInboundMessage(
|
|
397
|
-
normalized: NormalizedIcqqGuildInbound,
|
|
398
|
-
): Promise<void> {
|
|
399
|
-
const message = this.$formatGuildMessage(normalized);
|
|
400
|
-
this.adapter.emit("message.receive", message);
|
|
401
|
-
this.logger.debug(
|
|
402
|
-
`${this.$id} recv channel(${message.$channel.id}):${segment.raw(message.$content)}`,
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
private $formatGuildMessage(
|
|
407
|
-
normalized: NormalizedIcqqGuildInbound,
|
|
408
|
-
): ReturnType<typeof Message.from<IpcGuildMessageEventData>> {
|
|
409
|
-
const senderInfo: IcqqSenderInfo = {
|
|
410
|
-
id: normalized.userId,
|
|
411
|
-
name: normalized.nickname,
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
const result = Message.from(normalized.raw, {
|
|
415
|
-
$id: normalized.messageId,
|
|
416
|
-
$adapter: "icqq" as const,
|
|
417
|
-
$endpoint: this.$config.name,
|
|
418
|
-
$sender: senderInfo,
|
|
419
|
-
$channel: {
|
|
420
|
-
id: normalized.channelId,
|
|
421
|
-
type: "channel",
|
|
422
|
-
parent: { type: "guild", id: normalized.guildId },
|
|
423
|
-
},
|
|
424
|
-
$content: normalized.content,
|
|
425
|
-
$raw: normalized.rawMessage,
|
|
426
|
-
$timestamp: normalized.timestampMs,
|
|
427
|
-
$recall: async () => {
|
|
428
|
-
await this.$recallMessage(result.$id);
|
|
429
|
-
},
|
|
430
|
-
$reply: async (
|
|
431
|
-
content: SendContent,
|
|
432
|
-
quote?: boolean | string,
|
|
433
|
-
): Promise<string> => {
|
|
434
|
-
if (!Array.isArray(content)) content = [content];
|
|
435
|
-
if (quote) {
|
|
436
|
-
content.unshift({
|
|
437
|
-
type: "reply",
|
|
438
|
-
data: { id: typeof quote === "boolean" ? result.$id : quote },
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
return await this.adapter.sendMessage({
|
|
442
|
-
context: "icqq",
|
|
443
|
-
endpoint: this.$config.name,
|
|
444
|
-
id: normalized.channelId,
|
|
445
|
-
type: "channel",
|
|
446
|
-
parent: { type: "guild", id: normalized.guildId },
|
|
447
|
-
content,
|
|
448
|
-
});
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
return result;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
private handleNoticeEvent(event: IcqqIpcRawEvent): void {
|
|
455
|
-
const dedupeKey = resolveSideEventDedupeKey(event, "notice");
|
|
456
|
-
if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
|
|
457
|
-
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
458
|
-
this.logger.debug(formatCompact({ ipc_dedupe: dedupeKey }));
|
|
459
|
-
}
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
const notice = formatIcqqNotice(event, this.$config.name);
|
|
463
|
-
this.adapter.emit("notice.receive", notice);
|
|
464
|
-
this.logger.debug(
|
|
465
|
-
formatCompact({
|
|
466
|
-
notice: `${notice.$type}.${notice.$scene_type}.${notice.$sub_type}`,
|
|
467
|
-
scene: `${notice.$scene_type}(${notice.$scene_id})`,
|
|
468
|
-
endpoint: this.$id,
|
|
469
|
-
}),
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
private handleRequestEvent(event: IcqqIpcRawEvent): void {
|
|
474
|
-
const dedupeKey = resolveSideEventDedupeKey(event, "request");
|
|
475
|
-
if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
|
|
476
|
-
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
477
|
-
this.logger.debug(formatCompact({ ipc_dedupe: dedupeKey }));
|
|
478
|
-
}
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
const request = formatIcqqRequest(event, this.$config.name, this.ipc);
|
|
482
|
-
this.adapter.emit("request.receive", request);
|
|
483
|
-
this.logger.debug(
|
|
484
|
-
formatCompact({
|
|
485
|
-
request: `${request.$type}.${request.$scene_type}.${request.$sub_type}`,
|
|
486
|
-
scene: `${request.$scene_type}(${request.$scene_id})`,
|
|
487
|
-
endpoint: this.$id,
|
|
488
|
-
from: request.$actor.id,
|
|
489
|
-
}),
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
private handleMetaEvent(event: IcqqIpcRawEvent): void {
|
|
494
|
-
const time = event.time ?? 0;
|
|
495
|
-
const kind = event.meta_event_type ?? event.post_type ?? 'meta';
|
|
496
|
-
const dedupeKey = `meta:${time}_${kind}_${event.sub_type ?? ''}`;
|
|
497
|
-
if (!this.inboundDeduper.shouldProcess(dedupeKey)) return;
|
|
498
|
-
|
|
499
|
-
this.logger.debug(formatIcqqMetaLog(event));
|
|
500
|
-
if (shouldRefreshListsOnMeta(event)) {
|
|
501
|
-
void this.refreshLists().catch((e: unknown) => {
|
|
502
|
-
this.logger.warn(
|
|
503
|
-
formatCompact({
|
|
504
|
-
op: "refresh_lists",
|
|
505
|
-
ok: false,
|
|
506
|
-
error: e instanceof Error ? e.message : String(e),
|
|
507
|
-
}),
|
|
508
|
-
);
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/** 调试 IPC 入站字段:默认 debug;设 ICQQ_IPC_LOG_RAW=1 则 info 打完整 payload */
|
|
514
|
-
private logIpcInboundPayload(
|
|
515
|
-
data: IcqqIpcMessageEvent,
|
|
516
|
-
normalized: NormalizedIcqqInbound,
|
|
517
|
-
): void {
|
|
518
|
-
const ext = data as IcqqIpcMessageEvent & Record<string, unknown>;
|
|
519
|
-
const keys = Object.keys(ext);
|
|
520
|
-
const idHints: Record<string, unknown> = {};
|
|
521
|
-
for (const key of keys) {
|
|
522
|
-
if (/message|msg|seq|id|source/i.test(key)) {
|
|
523
|
-
idHints[key] = ext[key];
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
const preview = JSON.stringify(data);
|
|
527
|
-
const compact = formatCompact({
|
|
528
|
-
post_type: data.post_type ?? "legacy",
|
|
529
|
-
message_type: data.message_type ?? data.type,
|
|
530
|
-
ipc_message_id: normalized.messageId,
|
|
531
|
-
id_source: normalized.idSource,
|
|
532
|
-
ipc_keys: keys.join(","),
|
|
533
|
-
...(Object.keys(idHints).length ? { ipc_id_hints: JSON.stringify(idHints) } : {}),
|
|
534
|
-
});
|
|
535
|
-
this.logger.debug(compact);
|
|
536
|
-
this.logger.debug(formatCompact({ ipc_payload: preview.slice(0, 1200) }));
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
$formatMessage(
|
|
540
|
-
input: NormalizedIcqqInbound | IcqqIpcMessageEvent,
|
|
541
|
-
): ReturnType<typeof Message.from<IcqqIpcMessageEvent>> {
|
|
542
|
-
const normalized =
|
|
543
|
-
"messageId" in input
|
|
544
|
-
? input
|
|
545
|
-
: normalizeIcqqInboundMessage(input);
|
|
546
|
-
if (!normalized) {
|
|
547
|
-
throw new Error("无法解析 icqq 入站消息");
|
|
200
|
+
for (const sub of this.#subscriptions.splice(0)) {
|
|
201
|
+
await sub.unsubscribe().catch(() => { /* ignore */ });
|
|
548
202
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const quoteId =
|
|
566
|
-
Message.quoteIdFromContent(normalized.content) ??
|
|
567
|
-
resolveIcqqQuoteIdFromEvent(normalized.raw);
|
|
568
|
-
Message.alignReplySegments(normalized.content, quoteId);
|
|
569
|
-
|
|
570
|
-
const result = Message.from(raw, {
|
|
571
|
-
$id: normalized.messageId,
|
|
572
|
-
$adapter: "icqq" as const,
|
|
573
|
-
$endpoint: this.$config.name,
|
|
574
|
-
$sender: senderInfo,
|
|
575
|
-
$channel: {
|
|
576
|
-
id: normalized.channelId,
|
|
577
|
-
type: normalized.channelType,
|
|
578
|
-
...(normalized.channelParentGroupId
|
|
579
|
-
? { parent: { type: 'group' as const, id: normalized.channelParentGroupId } }
|
|
580
|
-
: {}),
|
|
581
|
-
},
|
|
582
|
-
$content: normalized.content,
|
|
583
|
-
$quote_id: quoteId,
|
|
584
|
-
$raw: normalized.rawMessage,
|
|
585
|
-
$timestamp: normalized.timestampMs,
|
|
586
|
-
$recall: async () => {
|
|
587
|
-
if (normalized.idSource === "synthetic") {
|
|
588
|
-
this.logger.warn(formatCompact( {
|
|
589
|
-
op: "recall",
|
|
590
|
-
endpoint: this.$id,
|
|
591
|
-
ok: false,
|
|
592
|
-
error: "no message_id in push",
|
|
593
|
-
}));
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
await this.$recallMessage(result.$id);
|
|
597
|
-
},
|
|
598
|
-
$reply: async (
|
|
599
|
-
content: SendContent,
|
|
600
|
-
quote?: boolean | string,
|
|
601
|
-
): Promise<string> => {
|
|
602
|
-
if (!Array.isArray(content)) content = [content];
|
|
603
|
-
if (quote) {
|
|
604
|
-
content.unshift({
|
|
605
|
-
type: "reply",
|
|
606
|
-
data: { id: typeof quote === "boolean" ? result.$id : quote },
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
return await this.adapter.sendMessage({
|
|
610
|
-
...result.$channel,
|
|
611
|
-
context: "icqq",
|
|
612
|
-
endpoint: this.$config.name,
|
|
613
|
-
content,
|
|
614
|
-
});
|
|
615
|
-
},
|
|
616
|
-
});
|
|
617
|
-
return result;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* 有 source.message_id 时用 get_msg 拉全量正文;否则仅用 source 内联摘要。
|
|
622
|
-
*/
|
|
623
|
-
private async primeQuotedSourceCache(source: unknown): Promise<void> {
|
|
624
|
-
if (!source) return;
|
|
625
|
-
const quoteId = resolveQuoteIdFromIcqqSource(source);
|
|
626
|
-
const s = source as { message_id?: unknown };
|
|
627
|
-
const hasCanonicalId =
|
|
628
|
-
quoteId &&
|
|
629
|
-
s.message_id != null &&
|
|
630
|
-
String(s.message_id).trim() === quoteId;
|
|
631
|
-
|
|
632
|
-
if (hasCanonicalId) {
|
|
633
|
-
try {
|
|
634
|
-
await this.fetchQuotedMessagePayload(quoteId);
|
|
635
|
-
return;
|
|
636
|
-
} catch (e: unknown) {
|
|
637
|
-
this.logger.debug(
|
|
638
|
-
formatCompact({
|
|
639
|
-
op: "quote_get_msg",
|
|
640
|
-
message_id: quoteId,
|
|
641
|
-
ok: false,
|
|
642
|
-
error: e instanceof Error ? e.message : String(e),
|
|
643
|
-
}),
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const sourcePayload = quotedPayloadFromIcqqSource(source);
|
|
649
|
-
if (!sourcePayload) return;
|
|
650
|
-
const enriched = await enrichQuotedPayloadWithForward(
|
|
651
|
-
this.ipc,
|
|
652
|
-
sourcePayload,
|
|
653
|
-
);
|
|
654
|
-
this.quotedSourceCache.set(enriched.messageId, enriched);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
private async fetchQuotedMessagePayload(
|
|
658
|
-
messageId: string,
|
|
659
|
-
): Promise<QuotedMessagePayload> {
|
|
660
|
-
const resp = await this.ipc.request(Actions.GET_MSG, {
|
|
661
|
-
message_id: messageId,
|
|
662
|
-
});
|
|
663
|
-
if (!resp.ok) {
|
|
664
|
-
throw new Error(resp.error ?? "get_msg failed");
|
|
665
|
-
}
|
|
666
|
-
const payload = parseIcqqGetMsgResponse(messageId, resp.data);
|
|
667
|
-
const enriched = await enrichQuotedPayloadWithForward(
|
|
668
|
-
this.ipc,
|
|
669
|
-
payload,
|
|
670
|
-
resp.data,
|
|
671
|
-
);
|
|
672
|
-
if (
|
|
673
|
-
isForwardPlaceholderPayload(enriched) &&
|
|
674
|
-
!String(
|
|
675
|
-
Array.isArray(enriched.content)
|
|
676
|
-
? segment.raw(enriched.content)
|
|
677
|
-
: enriched.content ?? "",
|
|
678
|
-
).includes("[Merged chat history")
|
|
679
|
-
) {
|
|
680
|
-
this.logger.debug(
|
|
681
|
-
formatCompact({
|
|
682
|
-
op: "forward_unresolved",
|
|
683
|
-
message_id: messageId,
|
|
684
|
-
}),
|
|
685
|
-
);
|
|
686
|
-
}
|
|
687
|
-
this.quotedSourceCache.set(messageId, enriched);
|
|
688
|
-
return enriched;
|
|
203
|
+
this.#unregisterAgent?.();
|
|
204
|
+
this.#unregisterAgent = undefined;
|
|
205
|
+
this.#inboundDeduper.clear();
|
|
206
|
+
this.friends.clear();
|
|
207
|
+
this.groups.clear();
|
|
208
|
+
try {
|
|
209
|
+
this.ipc?.close();
|
|
210
|
+
} catch {
|
|
211
|
+
/* ignore */
|
|
212
|
+
}
|
|
213
|
+
// 置空 ipc:start 前 / stop 后的 request() 走防御性报错而非 TypeError
|
|
214
|
+
this.ipc = undefined as unknown as IcqqIpcTransport;
|
|
215
|
+
this.#started = false;
|
|
216
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
|
|
689
217
|
}
|
|
690
218
|
|
|
691
|
-
async
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
return enrichQuotedPayloadWithForward(this.ipc, cached);
|
|
219
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
220
|
+
if (!this.ipc) {
|
|
221
|
+
throw new Error(`icqq endpoint ${this.name} 未连接(IPC 未初始化或已停止),无法发送消息`);
|
|
695
222
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const resp = await this.ipc.request(Actions.RECALL_MSG, {
|
|
703
|
-
message_id: id,
|
|
704
|
-
});
|
|
705
|
-
if (!resp.ok) {
|
|
706
|
-
this.logger.warn(formatCompact( {
|
|
707
|
-
op: "recall",
|
|
708
|
-
endpoint: this.$id,
|
|
709
|
-
ok: false,
|
|
710
|
-
error: resp.error,
|
|
711
|
-
}));
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// ── 发送消息 ───────────────────────────────────────────────────────
|
|
716
|
-
|
|
717
|
-
async $sendMessage(options: SendOptions): Promise<string> {
|
|
718
|
-
const outboundMedia = resolveIcqqOutboundMediaMode(this.$config);
|
|
719
|
-
const expanded = expandInteractiveSegmentsInContent(options.content);
|
|
720
|
-
const qrcodeContent = expanded;
|
|
721
|
-
const content = materializeOutboundBase64(qrcodeContent, outboundMedia);
|
|
722
|
-
const message = buildIcqqIpcMessageImpl(content);
|
|
723
|
-
|
|
223
|
+
const mediaMode = resolveIcqqOutboundMediaMode(this.#options.config);
|
|
224
|
+
const content = Array.isArray(payload)
|
|
225
|
+
? materializeOutboundBase64(payload, mediaMode)
|
|
226
|
+
: payload;
|
|
227
|
+
const message = formatOutboundBody(content);
|
|
228
|
+
const parsed = parseSendTarget(target);
|
|
724
229
|
let action: string;
|
|
725
230
|
let params: Record<string, unknown>;
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
options.parent?.type === "group" ? options.parent.id : undefined;
|
|
731
|
-
if (groupParent) {
|
|
732
|
-
action = Actions.SEND_TEMP_MSG;
|
|
733
|
-
params = {
|
|
734
|
-
group_id: Number(groupParent),
|
|
735
|
-
user_id: Number(options.id),
|
|
736
|
-
message,
|
|
737
|
-
};
|
|
738
|
-
} else {
|
|
739
|
-
action = Actions.SEND_PRIVATE_MSG;
|
|
740
|
-
params = { user_id: Number(options.id), message };
|
|
741
|
-
}
|
|
231
|
+
switch (parsed.kind) {
|
|
232
|
+
case 'private':
|
|
233
|
+
action = Actions.SEND_PRIVATE_MSG;
|
|
234
|
+
params = { user_id: parsed.userId, message };
|
|
742
235
|
break;
|
|
743
|
-
|
|
744
|
-
case "group":
|
|
236
|
+
case 'group':
|
|
745
237
|
action = Actions.SEND_GROUP_MSG;
|
|
746
|
-
params = { group_id:
|
|
238
|
+
params = { group_id: parsed.groupId, message };
|
|
747
239
|
break;
|
|
748
|
-
case
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
240
|
+
case 'temp':
|
|
241
|
+
action = Actions.SEND_TEMP_MSG;
|
|
242
|
+
params = {
|
|
243
|
+
group_id: parsed.groupId,
|
|
244
|
+
user_id: parsed.userId,
|
|
245
|
+
message,
|
|
246
|
+
};
|
|
247
|
+
break;
|
|
248
|
+
case 'channel':
|
|
756
249
|
action = Actions.GUILD_SEND_MSG;
|
|
757
250
|
params = {
|
|
758
|
-
guild_id: guildId,
|
|
759
|
-
channel_id:
|
|
251
|
+
guild_id: parsed.guildId,
|
|
252
|
+
channel_id: parsed.channelId,
|
|
760
253
|
message,
|
|
761
254
|
};
|
|
762
255
|
break;
|
|
763
|
-
}
|
|
764
|
-
default:
|
|
765
|
-
throw new Error(`不支持的频道类型: ${options.type}`);
|
|
766
256
|
}
|
|
767
|
-
|
|
768
257
|
const resp = await this.ipc.request(action, params);
|
|
769
258
|
if (!resp.ok) {
|
|
770
|
-
this.logger.debug(formatCompact({
|
|
771
|
-
op: action,
|
|
772
|
-
...params,
|
|
773
|
-
}));
|
|
774
259
|
throw new Error(`发送消息失败: ${resp.error}`);
|
|
775
260
|
}
|
|
776
|
-
|
|
777
261
|
const messageId = String(
|
|
778
|
-
(resp.data as
|
|
779
|
-
);
|
|
780
|
-
this.logger.debug(
|
|
781
|
-
`${this.$id} send ${options.type}(${options.id}):${segment.raw(options.content)}`,
|
|
262
|
+
(resp.data as { message_id?: unknown } | undefined)?.message_id ?? `sent_${Date.now()}`,
|
|
782
263
|
);
|
|
264
|
+
logger.debug(formatCompact({
|
|
265
|
+
op: 'icqq_send',
|
|
266
|
+
endpoint: this.name,
|
|
267
|
+
target,
|
|
268
|
+
messageId,
|
|
269
|
+
}));
|
|
783
270
|
return messageId;
|
|
784
271
|
}
|
|
785
272
|
|
|
786
|
-
// ──
|
|
273
|
+
// ── 消息回应(activity-feedback / typing reaction)────────────────────
|
|
787
274
|
|
|
788
|
-
/**
|
|
789
|
-
* 表情符号到 reaction id 的映射
|
|
790
|
-
* QQ 使用数字 ID 来标识表情,而不是 Unicode 字符
|
|
791
|
-
*/
|
|
792
275
|
private static readonly EMOJI_MAP: Record<string, string> = {
|
|
793
|
-
'⏳': '1468368274',
|
|
794
|
-
'👍': '128077',
|
|
795
|
-
'❤️': '10084',
|
|
796
|
-
'😊': '128522',
|
|
797
|
-
'🎉': '127881',
|
|
798
|
-
'🔥': '128293',
|
|
799
|
-
'✅': '9989',
|
|
800
|
-
'❌': '10060',
|
|
801
|
-
'⭐': '11088',
|
|
802
|
-
'💯': '128175',
|
|
276
|
+
'⏳': '1468368274',
|
|
277
|
+
'👍': '128077',
|
|
278
|
+
'❤️': '10084',
|
|
279
|
+
'😊': '128522',
|
|
280
|
+
'🎉': '127881',
|
|
281
|
+
'🔥': '128293',
|
|
282
|
+
'✅': '9989',
|
|
283
|
+
'❌': '10060',
|
|
284
|
+
'⭐': '11088',
|
|
285
|
+
'💯': '128175',
|
|
803
286
|
};
|
|
804
287
|
|
|
805
|
-
/**
|
|
806
|
-
* 将表情符号转换为 reaction id
|
|
807
|
-
*/
|
|
808
288
|
private getEmojiId(emoji: string): string {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
return emoji;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// 从映射中查找
|
|
815
|
-
const id = IcqqEndpoint.EMOJI_MAP[emoji];
|
|
816
|
-
if (id) {
|
|
817
|
-
return id;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// 默认返回 Unicode 码点
|
|
821
|
-
return String(emoji.codePointAt(0) || 0);
|
|
289
|
+
if (/^\d+$/u.test(emoji)) return emoji;
|
|
290
|
+
return IcqqIpcEndpoint.EMOJI_MAP[emoji] ?? String(emoji.codePointAt(0) || 0);
|
|
822
291
|
}
|
|
823
292
|
|
|
824
|
-
/**
|
|
825
|
-
|
|
826
|
-
*
|
|
827
|
-
* @param messageId - 消息 ID
|
|
828
|
-
* @param emoji - 表情符号或 reaction id
|
|
829
|
-
* @returns 反应 ID,可用于后续移除
|
|
830
|
-
*/
|
|
831
|
-
async $addReaction(messageId: string, emoji: string): Promise<string | null> {
|
|
293
|
+
/** Activity-feedback / OutboundHost reaction surface. */
|
|
294
|
+
async addReaction(messageId: string, emoji: string): Promise<string | null> {
|
|
832
295
|
try {
|
|
833
296
|
const emojiId = this.getEmojiId(emoji);
|
|
834
|
-
|
|
835
|
-
const resp = await this.ipc.request(Actions.GROUP_SET_REACTION, {
|
|
297
|
+
const resp = await this.request(Actions.GROUP_SET_REACTION, {
|
|
836
298
|
message_id: messageId,
|
|
837
299
|
id: emojiId,
|
|
838
300
|
});
|
|
839
|
-
|
|
840
301
|
if (!resp.ok) {
|
|
841
|
-
|
|
842
|
-
op:
|
|
843
|
-
endpoint: this
|
|
302
|
+
logger.warn(formatCompact({
|
|
303
|
+
op: 'add_reaction',
|
|
304
|
+
endpoint: this.name,
|
|
844
305
|
message_id: messageId,
|
|
845
306
|
emoji,
|
|
846
307
|
id: emojiId,
|
|
@@ -849,22 +310,19 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
849
310
|
}));
|
|
850
311
|
return null;
|
|
851
312
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
endpoint: this.$id,
|
|
313
|
+
logger.debug(formatCompact({
|
|
314
|
+
op: 'add_reaction',
|
|
315
|
+
endpoint: this.name,
|
|
856
316
|
message_id: messageId,
|
|
857
317
|
emoji,
|
|
858
318
|
id: emojiId,
|
|
859
319
|
ok: true,
|
|
860
320
|
}));
|
|
861
|
-
|
|
862
|
-
// 返回 reaction id,供 $removeReaction 直接使用
|
|
863
321
|
return emojiId;
|
|
864
322
|
} catch (error) {
|
|
865
|
-
|
|
866
|
-
op:
|
|
867
|
-
endpoint: this
|
|
323
|
+
logger.warn(formatCompact({
|
|
324
|
+
op: 'add_reaction',
|
|
325
|
+
endpoint: this.name,
|
|
868
326
|
message_id: messageId,
|
|
869
327
|
emoji,
|
|
870
328
|
ok: false,
|
|
@@ -874,61 +332,214 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
874
332
|
}
|
|
875
333
|
}
|
|
876
334
|
|
|
877
|
-
|
|
878
|
-
* 移除消息回应
|
|
879
|
-
*
|
|
880
|
-
* @param messageId - 消息 ID
|
|
881
|
-
* @param reactionId - 反应 ID(由 $addReaction 返回)
|
|
882
|
-
*/
|
|
883
|
-
async $removeReaction(messageId: string, reactionId: string): Promise<void> {
|
|
335
|
+
async removeReaction(messageId: string, reactionId: string): Promise<void> {
|
|
884
336
|
try {
|
|
885
|
-
// 兼容两种格式:
|
|
886
|
-
// 1) 旧格式 reaction_{id}_{timestamp}
|
|
887
|
-
// 2) 新格式直接为 id
|
|
888
337
|
const parts = reactionId.split('_');
|
|
889
|
-
const emojiId = parts.length >= 2 ? parts[1] : reactionId;
|
|
890
|
-
|
|
891
|
-
const resp = await this.ipc.request(Actions.GROUP_DEL_REACTION, {
|
|
338
|
+
const emojiId = parts.length >= 2 ? parts[1]! : reactionId;
|
|
339
|
+
const resp = await this.request(Actions.GROUP_DEL_REACTION, {
|
|
892
340
|
message_id: messageId,
|
|
893
341
|
id: emojiId,
|
|
894
342
|
});
|
|
895
|
-
|
|
896
343
|
if (!resp.ok) {
|
|
897
|
-
|
|
898
|
-
op:
|
|
899
|
-
endpoint: this
|
|
344
|
+
logger.warn(formatCompact({
|
|
345
|
+
op: 'remove_reaction',
|
|
346
|
+
endpoint: this.name,
|
|
900
347
|
message_id: messageId,
|
|
901
348
|
reaction_id: reactionId,
|
|
902
349
|
id: emojiId,
|
|
903
350
|
ok: false,
|
|
904
351
|
error: resp.error,
|
|
905
352
|
}));
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
logger.debug(formatCompact({
|
|
356
|
+
op: 'remove_reaction',
|
|
357
|
+
endpoint: this.name,
|
|
358
|
+
message_id: messageId,
|
|
359
|
+
reaction_id: reactionId,
|
|
360
|
+
id: emojiId,
|
|
361
|
+
ok: true,
|
|
362
|
+
}));
|
|
363
|
+
} catch (error) {
|
|
364
|
+
logger.warn(formatCompact({
|
|
365
|
+
op: 'remove_reaction',
|
|
366
|
+
endpoint: this.name,
|
|
367
|
+
message_id: messageId,
|
|
368
|
+
reaction_id: reactionId,
|
|
369
|
+
ok: false,
|
|
370
|
+
error: error instanceof Error ? error.message : String(error),
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Activity-feedback autoRemove / Console recall. */
|
|
376
|
+
async recallMessage(messageId: string): Promise<void> {
|
|
377
|
+
if (!messageId || messageId.startsWith('outbound:')) return;
|
|
378
|
+
try {
|
|
379
|
+
const resp = await this.request(Actions.RECALL_MSG, { message_id: messageId });
|
|
380
|
+
if (!resp.ok) {
|
|
381
|
+
logger.warn(formatCompact({
|
|
382
|
+
op: 'recall_message',
|
|
383
|
+
endpoint: this.name,
|
|
910
384
|
message_id: messageId,
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
ok: true,
|
|
385
|
+
ok: false,
|
|
386
|
+
error: resp.error,
|
|
914
387
|
}));
|
|
388
|
+
return;
|
|
915
389
|
}
|
|
390
|
+
logger.debug(formatCompact({
|
|
391
|
+
op: 'recall_message',
|
|
392
|
+
endpoint: this.name,
|
|
393
|
+
message_id: messageId,
|
|
394
|
+
ok: true,
|
|
395
|
+
}));
|
|
916
396
|
} catch (error) {
|
|
917
|
-
|
|
918
|
-
op:
|
|
919
|
-
endpoint: this
|
|
397
|
+
logger.warn(formatCompact({
|
|
398
|
+
op: 'recall_message',
|
|
399
|
+
endpoint: this.name,
|
|
920
400
|
message_id: messageId,
|
|
921
|
-
reaction_id: reactionId,
|
|
922
401
|
ok: false,
|
|
923
402
|
error: error instanceof Error ? error.message : String(error),
|
|
924
403
|
}));
|
|
925
404
|
}
|
|
926
405
|
}
|
|
406
|
+
|
|
407
|
+
/** Test / internal: admit when open. */
|
|
408
|
+
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 });
|
|
412
|
+
void this.#options.gateway.receive({
|
|
413
|
+
adapter: this.#options.id,
|
|
414
|
+
target: msg.target,
|
|
415
|
+
content: msg.content,
|
|
416
|
+
sender: msg.sender,
|
|
417
|
+
id: msg.id,
|
|
418
|
+
metadata: Object.freeze({
|
|
419
|
+
endpoint: this.name,
|
|
420
|
+
channelType: msg.channelType,
|
|
421
|
+
...msg.metadata,
|
|
422
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
423
|
+
}),
|
|
424
|
+
}).catch((err) => {
|
|
425
|
+
logger.warn(formatCompact({
|
|
426
|
+
op: 'icqq_gateway_receive_failed',
|
|
427
|
+
target: msg.target,
|
|
428
|
+
error: err instanceof Error ? err.message : String(err),
|
|
429
|
+
}));
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async #refreshLists(): Promise<void> {
|
|
434
|
+
const [flResp, glResp] = await Promise.all([
|
|
435
|
+
this.ipc.request(Actions.LIST_FRIENDS),
|
|
436
|
+
this.ipc.request(Actions.LIST_GROUPS),
|
|
437
|
+
]);
|
|
438
|
+
this.friends.clear();
|
|
439
|
+
if (flResp.ok && Array.isArray(flResp.data)) {
|
|
440
|
+
for (const f of flResp.data as IpcFriendInfo[]) {
|
|
441
|
+
this.friends.set(f.user_id, f);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
this.groups.clear();
|
|
445
|
+
if (glResp.ok && Array.isArray(glResp.data)) {
|
|
446
|
+
for (const g of glResp.data as IpcGroupInfo[]) {
|
|
447
|
+
this.groups.set(g.group_id, g);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
#handleEvent(event: IpcEvent): void {
|
|
453
|
+
if (isIcqqGuildIpcEvent(event.event)) {
|
|
454
|
+
this.#handleGuildEvent(event);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const payload = unwrapIcqqIpcEventPayload(event);
|
|
458
|
+
if (!payload || typeof payload !== 'object') return;
|
|
459
|
+
if (!isIcqqMessagePostType(payload)) return;
|
|
460
|
+
const data = payload as IcqqIpcMessageEvent;
|
|
461
|
+
if (shouldSkipSelfInboundMessage(data)) return;
|
|
462
|
+
const normalized = normalizeIcqqInboundMessage(data);
|
|
463
|
+
if (!normalized) return;
|
|
464
|
+
if (!this.#inboundDeduper.shouldProcess(normalized.messageId)) return;
|
|
465
|
+
this.admit({
|
|
466
|
+
id: normalized.messageId,
|
|
467
|
+
target: formatInboundTarget({
|
|
468
|
+
channelType: normalized.channelType,
|
|
469
|
+
channelId: normalized.channelId,
|
|
470
|
+
channelParentGroupId: normalized.channelParentGroupId,
|
|
471
|
+
}),
|
|
472
|
+
content: formatInboundContent(normalized.rawMessage),
|
|
473
|
+
sender: normalized.userId,
|
|
474
|
+
channelType: normalized.channelType,
|
|
475
|
+
metadata: buildIcqqQuoteMetadata(data, {
|
|
476
|
+
nickname: normalized.nickname,
|
|
477
|
+
senderRole: normalized.senderRole,
|
|
478
|
+
}),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#handleGuildEvent(event: IpcEvent): void {
|
|
483
|
+
const payload = event.data;
|
|
484
|
+
if (!payload || typeof payload !== 'object') return;
|
|
485
|
+
const normalized = normalizeIcqqGuildInboundMessage(
|
|
486
|
+
payload as Parameters<typeof normalizeIcqqGuildInboundMessage>[0],
|
|
487
|
+
);
|
|
488
|
+
if (!normalized) return;
|
|
489
|
+
if (!this.#inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) return;
|
|
490
|
+
this.admit({
|
|
491
|
+
id: normalized.messageId,
|
|
492
|
+
target: formatInboundTarget({
|
|
493
|
+
channelType: 'channel',
|
|
494
|
+
channelId: normalized.channelId,
|
|
495
|
+
guildId: normalized.guildId,
|
|
496
|
+
}),
|
|
497
|
+
content: formatInboundContent(normalized.rawMessage),
|
|
498
|
+
sender: normalized.userId,
|
|
499
|
+
channelType: 'channel',
|
|
500
|
+
metadata: {
|
|
501
|
+
guildId: normalized.guildId,
|
|
502
|
+
nickname: normalized.nickname,
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
}
|
|
927
506
|
}
|
|
928
507
|
|
|
929
|
-
/**
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
508
|
+
/**
|
|
509
|
+
* 组装入站 metadata:在基础字段上补充 quote 链路信息。
|
|
510
|
+
* - quote_id:被引用消息 id(resolveIcqqQuoteIdFromEvent,无则不写)
|
|
511
|
+
* - quote_sender_id / quote_sender_name / quote_content:source 可用时的平铺摘要
|
|
512
|
+
*/
|
|
513
|
+
function buildIcqqQuoteMetadata(
|
|
514
|
+
data: IcqqIpcMessageEvent,
|
|
515
|
+
base: Record<string, unknown>,
|
|
516
|
+
): Record<string, unknown> {
|
|
517
|
+
const metadata: Record<string, unknown> = { ...base };
|
|
518
|
+
const quoteId = resolveIcqqQuoteIdFromEvent(data);
|
|
519
|
+
if (quoteId) metadata.quote_id = quoteId;
|
|
520
|
+
const quoted = quotedPayloadFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
521
|
+
if (quoted?.sender?.id) metadata.quote_sender_id = quoted.sender.id;
|
|
522
|
+
if (quoted?.sender?.name) metadata.quote_sender_name = quoted.sender.name;
|
|
523
|
+
const quoteText = quoted
|
|
524
|
+
? (typeof quoted.content === 'string'
|
|
525
|
+
? quoted.content
|
|
526
|
+
: quoted.content
|
|
527
|
+
.map((seg) =>
|
|
528
|
+
seg && typeof seg === 'object' && seg.type === 'text'
|
|
529
|
+
? String((seg.data as { text?: unknown } | undefined)?.text ?? '')
|
|
530
|
+
: '',
|
|
531
|
+
)
|
|
532
|
+
.join('')
|
|
533
|
+
).trim() || quoted.raw || ''
|
|
534
|
+
: '';
|
|
535
|
+
if (quoteText) metadata.quote_content = quoteText;
|
|
536
|
+
return metadata;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function defaultCreateIpc(config: ResolvedIcqqConfig): Promise<IcqqIpcTransport> {
|
|
540
|
+
const uin = Number(config.name);
|
|
541
|
+
if (config.rpc) {
|
|
542
|
+
return IpcClient.connectRpc(config.rpc);
|
|
543
|
+
}
|
|
544
|
+
return IpcClient.connect(uin);
|
|
934
545
|
}
|