@zhin.js/adapter-icqq 6.0.0 → 6.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +3 -3
- package/{skills/icqq → agent}/PERMITS.md +6 -6
- package/agent/tools/announce.ts +21 -0
- package/agent/tools/essence.ts +22 -0
- package/agent/tools/friend_list.ts +19 -0
- package/agent/tools/group_files.ts +27 -0
- package/agent/tools/group_invite.ts +20 -0
- package/agent/tools/group_list.ts +20 -0
- package/agent/tools/group_sign.ts +19 -0
- package/agent/tools/list_muted.ts +22 -0
- package/agent/tools/poke.ts +20 -0
- package/agent/tools/send_user_like.ts +22 -0
- package/agent/tools/set_anonymous.ts +22 -0
- package/agent/tools/set_title.ts +25 -0
- package/dist/index.js +2 -2
- package/lib/agent/tools/announce.js +21 -0
- package/lib/agent/tools/announce.js.map +1 -0
- package/lib/agent/tools/essence.js +22 -0
- package/lib/agent/tools/essence.js.map +1 -0
- package/lib/agent/tools/friend_list.js +19 -0
- package/lib/agent/tools/friend_list.js.map +1 -0
- package/lib/agent/tools/group_files.js +29 -0
- package/lib/agent/tools/group_files.js.map +1 -0
- package/lib/agent/tools/group_invite.js +20 -0
- package/lib/agent/tools/group_invite.js.map +1 -0
- package/lib/agent/tools/group_list.js +20 -0
- package/lib/agent/tools/group_list.js.map +1 -0
- package/lib/agent/tools/group_sign.js +19 -0
- package/lib/agent/tools/group_sign.js.map +1 -0
- package/lib/agent/tools/list_muted.js +23 -0
- package/lib/agent/tools/list_muted.js.map +1 -0
- package/lib/agent/tools/poke.js +20 -0
- package/lib/agent/tools/poke.js.map +1 -0
- package/lib/agent/tools/send_user_like.js +22 -0
- package/lib/agent/tools/send_user_like.js.map +1 -0
- package/lib/agent/tools/set_anonymous.js +22 -0
- package/lib/agent/tools/set_anonymous.js.map +1 -0
- package/lib/agent/tools/set_title.js +25 -0
- package/lib/agent/tools/set_title.js.map +1 -0
- package/lib/{adapter.js → src/adapter.js} +13 -6
- package/lib/src/adapter.js.map +1 -0
- package/lib/src/agent-prompt.js.map +1 -0
- package/lib/{cq-message.js → src/cq-message.js} +12 -3
- package/lib/src/cq-message.js.map +1 -0
- package/lib/{endpoint.js → src/endpoint.js} +130 -63
- package/lib/src/endpoint.js.map +1 -0
- package/lib/{forward-msg.js → src/forward-msg.js} +4 -1
- package/lib/src/forward-msg.js.map +1 -0
- package/lib/src/get-msg.js.map +1 -0
- package/lib/src/icqq-agent-deps.js +10 -0
- package/lib/src/icqq-agent-deps.js.map +1 -0
- package/lib/src/icqq-guild.js +115 -0
- package/lib/src/icqq-guild.js.map +1 -0
- package/lib/{icqq-inbound.js → src/icqq-inbound.js} +15 -5
- package/lib/src/icqq-inbound.js.map +1 -0
- package/lib/src/icqq-login-assist-bridge.js.map +1 -0
- package/lib/{icqq-side-events.js → src/icqq-side-events.js} +28 -92
- package/lib/src/icqq-side-events.js.map +1 -0
- package/lib/{index.js → src/index.js} +0 -1
- package/lib/src/index.js.map +1 -0
- package/lib/src/ipc-client.js.map +1 -0
- package/lib/src/login-assist-routes.js.map +1 -0
- package/lib/src/login-ipc-contract.js.map +1 -0
- package/lib/src/outbound-media.js.map +1 -0
- package/lib/src/protocol.js.map +1 -0
- package/lib/src/routes.js.map +1 -0
- package/lib/src/segment-mapper.js +2 -0
- package/lib/src/segment-mapper.js.map +1 -0
- package/lib/src/tools/index.js +23 -0
- package/lib/src/tools/index.js.map +1 -0
- package/lib/src/types.js.map +1 -0
- package/package.json +25 -21
- package/src/adapter.ts +13 -6
- package/src/agent-prompt.ts +1 -2
- package/src/cq-message.ts +11 -3
- package/src/endpoint.ts +158 -82
- package/src/forward-msg.ts +2 -2
- package/src/icqq-agent-deps.ts +18 -0
- package/src/icqq-guild.ts +157 -0
- package/src/icqq-inbound.ts +16 -5
- package/src/icqq-side-events.ts +33 -118
- package/src/index.ts +0 -8
- package/src/segment-mapper.ts +1 -0
- package/src/tools/index.ts +16 -397
- package/src/types.ts +0 -45
- package/lib/adapter.d.ts +0 -39
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js.map +0 -1
- package/lib/agent-prompt.d.ts +0 -3
- package/lib/agent-prompt.d.ts.map +0 -1
- package/lib/agent-prompt.js.map +0 -1
- package/lib/cq-message.d.ts +0 -10
- package/lib/cq-message.d.ts.map +0 -1
- package/lib/cq-message.js.map +0 -1
- package/lib/endpoint.d.ts +0 -99
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/forward-msg.d.ts +0 -27
- package/lib/forward-msg.d.ts.map +0 -1
- package/lib/forward-msg.js.map +0 -1
- package/lib/get-msg.d.ts +0 -3
- package/lib/get-msg.d.ts.map +0 -1
- package/lib/get-msg.js.map +0 -1
- package/lib/icqq-inbound.d.ts +0 -130
- package/lib/icqq-inbound.d.ts.map +0 -1
- package/lib/icqq-inbound.js.map +0 -1
- package/lib/icqq-login-assist-bridge.d.ts +0 -5
- package/lib/icqq-login-assist-bridge.d.ts.map +0 -1
- package/lib/icqq-login-assist-bridge.js.map +0 -1
- package/lib/icqq-side-events.d.ts +0 -34
- package/lib/icqq-side-events.d.ts.map +0 -1
- package/lib/icqq-side-events.js.map +0 -1
- package/lib/index.d.ts +0 -20
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/ipc-client.d.ts +0 -74
- package/lib/ipc-client.d.ts.map +0 -1
- package/lib/ipc-client.js.map +0 -1
- package/lib/login-assist-routes.d.ts +0 -7
- package/lib/login-assist-routes.d.ts.map +0 -1
- package/lib/login-assist-routes.js.map +0 -1
- package/lib/login-ipc-contract.d.ts +0 -44
- package/lib/login-ipc-contract.d.ts.map +0 -1
- package/lib/login-ipc-contract.js.map +0 -1
- package/lib/outbound-media.d.ts +0 -14
- package/lib/outbound-media.d.ts.map +0 -1
- package/lib/outbound-media.js.map +0 -1
- package/lib/protocol.d.ts +0 -198
- package/lib/protocol.d.ts.map +0 -1
- package/lib/protocol.js.map +0 -1
- package/lib/routes.d.ts +0 -8
- package/lib/routes.d.ts.map +0 -1
- package/lib/routes.js.map +0 -1
- package/lib/tools/index.d.ts +0 -10
- package/lib/tools/index.d.ts.map +0 -1
- package/lib/tools/index.js +0 -341
- package/lib/tools/index.js.map +0 -1
- package/lib/types.d.ts +0 -117
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js.map +0 -1
- package/lib/typing-indicator-example.d.ts +0 -108
- package/lib/typing-indicator-example.d.ts.map +0 -1
- package/lib/typing-indicator-example.js +0 -220
- package/lib/typing-indicator-example.js.map +0 -1
- package/lib/typing-indicator.d.ts +0 -87
- package/lib/typing-indicator.d.ts.map +0 -1
- package/lib/typing-indicator.js +0 -229
- package/lib/typing-indicator.js.map +0 -1
- package/src/typing-indicator-example.ts +0 -269
- package/src/typing-indicator.ts +0 -317
- /package/lib/{agent-prompt.js → src/agent-prompt.js} +0 -0
- /package/lib/{get-msg.js → src/get-msg.js} +0 -0
- /package/lib/{icqq-login-assist-bridge.js → src/icqq-login-assist-bridge.js} +0 -0
- /package/lib/{ipc-client.js → src/ipc-client.js} +0 -0
- /package/lib/{login-assist-routes.js → src/login-assist-routes.js} +0 -0
- /package/lib/{login-ipc-contract.js → src/login-ipc-contract.js} +0 -0
- /package/lib/{outbound-media.js → src/outbound-media.js} +0 -0
- /package/lib/{protocol.js → src/protocol.js} +0 -0
- /package/lib/{routes.js → src/routes.js} +0 -0
- /package/lib/{types.js → src/types.js} +0 -0
package/src/endpoint.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 不再直接依赖 @icqqjs/icqq 协议库。
|
|
5
5
|
* 登录由 `icqq login` 完成,本 Endpoint 只负责连接守护进程并收发消息。
|
|
6
6
|
*/
|
|
7
|
-
import { formatCompact, Endpoint, Message, segment, SendContent, SendOptions, type QuotedMessagePayload,} from 'zhin.js';
|
|
7
|
+
import { formatCompact, Endpoint, Message, segment, SendContent, SendOptions, expandInteractiveSegmentsInContent, resolveSideEventDedupeKey, type QuotedMessagePayload,} from 'zhin.js';
|
|
8
8
|
import type {
|
|
9
9
|
IcqqEndpointConfig,
|
|
10
10
|
IcqqSenderInfo,
|
|
@@ -14,20 +14,9 @@ import type {
|
|
|
14
14
|
} from "./types.js";
|
|
15
15
|
import type { IcqqAdapter } from "./adapter.js";
|
|
16
16
|
import { IpcClient } from "./ipc-client.js";
|
|
17
|
-
import { Actions, type IpcEvent } from
|
|
18
|
-
import type
|
|
19
|
-
|
|
20
|
-
findIcqqNestedMessageSource,
|
|
21
|
-
InboundMessageDeduper,
|
|
22
|
-
isIcqqMessagePostType,
|
|
23
|
-
normalizeIcqqInboundMessage,
|
|
24
|
-
quotedPayloadFromIcqqSource,
|
|
25
|
-
resolveIcqqQuoteIdFromEvent,
|
|
26
|
-
resolveQuoteIdFromIcqqSource,
|
|
27
|
-
shouldSkipSelfInboundMessage,
|
|
28
|
-
unwrapIcqqIpcEventPayload,
|
|
29
|
-
type NormalizedIcqqInbound,
|
|
30
|
-
} from "./icqq-inbound.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
|
+
|
|
31
20
|
import {
|
|
32
21
|
buildIcqqIpcMessage as buildIcqqIpcMessageImpl,
|
|
33
22
|
parseCqMessage as parseCqMessageImpl,
|
|
@@ -45,7 +34,6 @@ import {
|
|
|
45
34
|
isIcqqNoticePayload,
|
|
46
35
|
isIcqqRequestPayload,
|
|
47
36
|
resolveIcqqEventPostType,
|
|
48
|
-
resolveSideEventDedupeKey,
|
|
49
37
|
shouldRefreshListsOnMeta,
|
|
50
38
|
type IcqqIpcRawEvent,
|
|
51
39
|
} from "./icqq-side-events.js";
|
|
@@ -56,9 +44,11 @@ import {
|
|
|
56
44
|
import { parseIcqqGetMsgResponse } from "./get-msg.js";
|
|
57
45
|
import { enrichQuotedPayloadWithForward, isForwardPlaceholderPayload } from "./forward-msg.js";
|
|
58
46
|
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
IcqqGuildCatalog,
|
|
48
|
+
isIcqqGuildIpcEvent,
|
|
49
|
+
normalizeIcqqGuildInboundMessage,
|
|
50
|
+
type NormalizedIcqqGuildInbound,
|
|
51
|
+
} from "./icqq-guild.js";
|
|
62
52
|
|
|
63
53
|
export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessageEvent> {
|
|
64
54
|
$connected = false;
|
|
@@ -69,6 +59,8 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
69
59
|
friends = new Map<number, IpcFriendInfo>();
|
|
70
60
|
/** 缓存的群列表 */
|
|
71
61
|
groups = new Map<number, IpcGroupInfo>();
|
|
62
|
+
/** QQ 频道(guild)子频道缓存 */
|
|
63
|
+
readonly guildCatalog = new IcqqGuildCatalog();
|
|
72
64
|
|
|
73
65
|
private subscriptions: Array<{ unsubscribe: () => Promise<void> }> = [];
|
|
74
66
|
/** 事件去重:覆盖多端回流/服务端重复推送等场景 */
|
|
@@ -80,9 +72,6 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
80
72
|
/** 是否已有重连循环在跑(避免多次 schedule 叠套) */
|
|
81
73
|
private reconnectRunning = false;
|
|
82
74
|
|
|
83
|
-
/** Typing Indicator 管理器 */
|
|
84
|
-
$typingIndicator?: ICQQTypingIndicatorManager;
|
|
85
|
-
|
|
86
75
|
get $id() {
|
|
87
76
|
return this.$config.name;
|
|
88
77
|
}
|
|
@@ -113,50 +102,13 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
113
102
|
|
|
114
103
|
await this.rebindIpcSession();
|
|
115
104
|
|
|
116
|
-
|
|
117
|
-
this.initTypingIndicator();
|
|
118
|
-
|
|
119
|
-
this.logger.info(formatCompact( {
|
|
105
|
+
this.logger.debug(formatCompact( {
|
|
120
106
|
机器人: this.$id,
|
|
121
107
|
好友数: this.friends.size,
|
|
122
108
|
群组数: this.groups.size,
|
|
123
|
-
思考提示: this.$typingIndicator ? '已启用' : '未启用',
|
|
124
109
|
}));
|
|
125
110
|
}
|
|
126
111
|
|
|
127
|
-
/**
|
|
128
|
-
* 初始化 Typing Indicator
|
|
129
|
-
* 根据配置自动启用或禁用
|
|
130
|
-
*/
|
|
131
|
-
private initTypingIndicator(): void {
|
|
132
|
-
const typingConfig = this.$config.typingIndicator;
|
|
133
|
-
|
|
134
|
-
// 如果配置中明确设置为 false,则不启用
|
|
135
|
-
if (typingConfig?.enabled === false) {
|
|
136
|
-
this.logger.debug(formatCompact({
|
|
137
|
-
endpoint: this.$id,
|
|
138
|
-
typingIndicator: 'disabled_by_config',
|
|
139
|
-
}));
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 启用 Typing Indicator
|
|
144
|
-
try {
|
|
145
|
-
this.$typingIndicator = enableTypingIndicator(this, typingConfig);
|
|
146
|
-
this.logger.debug(formatCompact({
|
|
147
|
-
endpoint: this.$id,
|
|
148
|
-
typingIndicator: 'enabled',
|
|
149
|
-
emoji: typingConfig?.defaultEmoji || '⏳',
|
|
150
|
-
}));
|
|
151
|
-
} catch (error) {
|
|
152
|
-
this.logger.warn(formatCompact({
|
|
153
|
-
endpoint: this.$id,
|
|
154
|
-
typingIndicator: 'failed',
|
|
155
|
-
error: error instanceof Error ? error.message : String(error),
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
112
|
/** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
|
|
161
113
|
private async rebindIpcSession(): Promise<void> {
|
|
162
114
|
const uin = Number(this.$config.name);
|
|
@@ -227,7 +179,7 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
227
179
|
|
|
228
180
|
try {
|
|
229
181
|
await this.rebindIpcSession();
|
|
230
|
-
this.logger.
|
|
182
|
+
this.logger.debug(formatCompact( {
|
|
231
183
|
op: "reconnect",
|
|
232
184
|
endpoint: this.$id,
|
|
233
185
|
friends: this.friends.size,
|
|
@@ -268,6 +220,27 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
268
220
|
this.groups.set(g.group_id, g);
|
|
269
221
|
}
|
|
270
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
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Console:列出 guild 子频道(含 parent.guild) */
|
|
235
|
+
getGuildChannelList(): ReturnType<IcqqGuildCatalog["getGuildChannelList"]> {
|
|
236
|
+
return this.guildCatalog.getGuildChannelList();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
resolveConsoleChannelNames(
|
|
240
|
+
channelId: string,
|
|
241
|
+
guildId?: string,
|
|
242
|
+
): { channelName?: string; parentName?: string } {
|
|
243
|
+
return this.guildCatalog.resolveConsoleChannelNames(channelId, guildId);
|
|
271
244
|
}
|
|
272
245
|
|
|
273
246
|
// ── 断开 ───────────────────────────────────────────────────────────
|
|
@@ -275,12 +248,6 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
275
248
|
async $disconnect(): Promise<void> {
|
|
276
249
|
this.intentionalDisconnect = true;
|
|
277
250
|
|
|
278
|
-
// 停止所有 Typing Indicator
|
|
279
|
-
if (this.$typingIndicator) {
|
|
280
|
-
await this.$typingIndicator.stopAll().catch(() => {});
|
|
281
|
-
this.$typingIndicator = undefined;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
251
|
this.ipc?.setOnRemoteDisconnect(null);
|
|
285
252
|
for (const sub of this.subscriptions) {
|
|
286
253
|
await sub.unsubscribe().catch(() => {});
|
|
@@ -290,7 +257,7 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
290
257
|
disposeIcqqLoginAssistBridge(this.$id);
|
|
291
258
|
this.ipc?.close();
|
|
292
259
|
this.$connected = false;
|
|
293
|
-
this.logger.
|
|
260
|
+
this.logger.debug(formatCompact( { op: "disconnect", endpoint: this.$id }));
|
|
294
261
|
}
|
|
295
262
|
|
|
296
263
|
// ── 消息处理 ───────────────────────────────────────────────────────
|
|
@@ -301,6 +268,23 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
301
268
|
return;
|
|
302
269
|
}
|
|
303
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
|
+
|
|
304
288
|
const payload = unwrapIcqqIpcEventPayload(event);
|
|
305
289
|
if (!payload || typeof payload !== "object") {
|
|
306
290
|
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
@@ -351,7 +335,7 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
351
335
|
|
|
352
336
|
if (shouldSkipSelfInboundMessage(data)) {
|
|
353
337
|
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
354
|
-
this.logger.
|
|
338
|
+
this.logger.debug(formatCompact({ ipc_skip: "self_message" }));
|
|
355
339
|
}
|
|
356
340
|
return;
|
|
357
341
|
}
|
|
@@ -359,7 +343,7 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
359
343
|
const normalized = normalizeIcqqInboundMessage(data);
|
|
360
344
|
if (!normalized) {
|
|
361
345
|
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
362
|
-
this.logger.
|
|
346
|
+
this.logger.debug(formatCompact({ ipc_skip: "normalize_failed" }));
|
|
363
347
|
}
|
|
364
348
|
return;
|
|
365
349
|
}
|
|
@@ -393,22 +377,95 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
393
377
|
);
|
|
394
378
|
}
|
|
395
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
|
+
|
|
396
454
|
private handleNoticeEvent(event: IcqqIpcRawEvent): void {
|
|
397
455
|
const dedupeKey = resolveSideEventDedupeKey(event, "notice");
|
|
398
456
|
if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
|
|
399
457
|
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
400
|
-
this.logger.
|
|
458
|
+
this.logger.debug(formatCompact({ ipc_dedupe: dedupeKey }));
|
|
401
459
|
}
|
|
402
460
|
return;
|
|
403
461
|
}
|
|
404
462
|
const notice = formatIcqqNotice(event, this.$config.name);
|
|
405
463
|
this.adapter.emit("notice.receive", notice);
|
|
406
|
-
this.logger.
|
|
464
|
+
this.logger.debug(
|
|
407
465
|
formatCompact({
|
|
408
|
-
notice: notice.$type
|
|
409
|
-
|
|
466
|
+
notice: `${notice.$type}.${notice.$scene_type}.${notice.$sub_type}`,
|
|
467
|
+
scene: `${notice.$scene_type}(${notice.$scene_id})`,
|
|
410
468
|
endpoint: this.$id,
|
|
411
|
-
sub_type: notice.$subType,
|
|
412
469
|
}),
|
|
413
470
|
);
|
|
414
471
|
}
|
|
@@ -417,24 +474,26 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
417
474
|
const dedupeKey = resolveSideEventDedupeKey(event, "request");
|
|
418
475
|
if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
|
|
419
476
|
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
420
|
-
this.logger.
|
|
477
|
+
this.logger.debug(formatCompact({ ipc_dedupe: dedupeKey }));
|
|
421
478
|
}
|
|
422
479
|
return;
|
|
423
480
|
}
|
|
424
481
|
const request = formatIcqqRequest(event, this.$config.name, this.ipc);
|
|
425
482
|
this.adapter.emit("request.receive", request);
|
|
426
|
-
this.logger.
|
|
483
|
+
this.logger.debug(
|
|
427
484
|
formatCompact({
|
|
428
|
-
request: request.$type
|
|
429
|
-
|
|
485
|
+
request: `${request.$type}.${request.$scene_type}.${request.$sub_type}`,
|
|
486
|
+
scene: `${request.$scene_type}(${request.$scene_id})`,
|
|
430
487
|
endpoint: this.$id,
|
|
431
|
-
from: request.$
|
|
488
|
+
from: request.$actor.id,
|
|
432
489
|
}),
|
|
433
490
|
);
|
|
434
491
|
}
|
|
435
492
|
|
|
436
493
|
private handleMetaEvent(event: IcqqIpcRawEvent): void {
|
|
437
|
-
const
|
|
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 ?? ''}`;
|
|
438
497
|
if (!this.inboundDeduper.shouldProcess(dedupeKey)) return;
|
|
439
498
|
|
|
440
499
|
this.logger.debug(formatIcqqMetaLog(event));
|
|
@@ -657,7 +716,8 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
657
716
|
|
|
658
717
|
async $sendMessage(options: SendOptions): Promise<string> {
|
|
659
718
|
const outboundMedia = resolveIcqqOutboundMediaMode(this.$config);
|
|
660
|
-
const
|
|
719
|
+
const expanded = expandInteractiveSegmentsInContent(options.content);
|
|
720
|
+
const qrcodeContent = expanded;
|
|
661
721
|
const content = materializeOutboundBase64(qrcodeContent, outboundMedia);
|
|
662
722
|
const message = buildIcqqIpcMessageImpl(content);
|
|
663
723
|
|
|
@@ -685,6 +745,22 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
685
745
|
action = Actions.SEND_GROUP_MSG;
|
|
686
746
|
params = { group_id: Number(options.id), message };
|
|
687
747
|
break;
|
|
748
|
+
case "channel": {
|
|
749
|
+
const guildId =
|
|
750
|
+
options.parent?.type === "guild" ? options.parent.id : undefined;
|
|
751
|
+
if (!guildId) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
'channel 发送需要 parent: { type: "guild", id: guild_id }',
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
action = Actions.GUILD_SEND_MSG;
|
|
757
|
+
params = {
|
|
758
|
+
guild_id: guildId,
|
|
759
|
+
channel_id: options.id,
|
|
760
|
+
message,
|
|
761
|
+
};
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
688
764
|
default:
|
|
689
765
|
throw new Error(`不支持的频道类型: ${options.type}`);
|
|
690
766
|
}
|
package/src/forward-msg.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 合并转发(聊天记录)解析:从 json/xml 提取 resid,经 IPC get_forward_msg 拉取正文。
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
5
|
-
|
|
4
|
+
import { segment, type MessageSegment, type QuotedMessagePayload } from 'zhin.js';
|
|
5
|
+
|
|
6
6
|
import { Actions } from "./protocol.js";
|
|
7
7
|
import type { IpcClient } from "./ipc-client.js";
|
|
8
8
|
import {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IcqqAdapter } from './adapter.js';
|
|
2
|
+
import type { IcqqEndpoint } from './endpoint.js';
|
|
3
|
+
|
|
4
|
+
export interface IcqqAgentDeps {
|
|
5
|
+
getEndpoint: (endpointId: string) => IcqqEndpoint;
|
|
6
|
+
getAdapter: () => IcqqAdapter;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let _deps: IcqqAgentDeps | null = null;
|
|
10
|
+
|
|
11
|
+
export function setIcqqAgentDeps(deps: IcqqAgentDeps): void {
|
|
12
|
+
_deps = deps;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getIcqqAgentDeps(): IcqqAgentDeps {
|
|
16
|
+
if (!_deps) throw new Error('icqq agent deps not initialized');
|
|
17
|
+
return _deps;
|
|
18
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICQQ guild (QQ 频道) catalog sync, inbound normalize, Console channel list.
|
|
3
|
+
*/
|
|
4
|
+
import type { MessageSegment } from "zhin.js";
|
|
5
|
+
import { Actions, type IpcGuildMessageEventData } from "./protocol.js";
|
|
6
|
+
import type { IpcClient } from "./ipc-client.js";
|
|
7
|
+
import { parseCqMessage } from "./cq-message.js";
|
|
8
|
+
import { toCanonicalSegments } from "./segment-mapper.js";
|
|
9
|
+
|
|
10
|
+
export interface IcqqGuildChannelEntry {
|
|
11
|
+
channel_id: string;
|
|
12
|
+
channel_name: string;
|
|
13
|
+
guild_id: string;
|
|
14
|
+
guild_name: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NormalizedIcqqGuildInbound {
|
|
18
|
+
messageId: string;
|
|
19
|
+
channelId: string;
|
|
20
|
+
guildId: string;
|
|
21
|
+
guildName: string;
|
|
22
|
+
channelName: string;
|
|
23
|
+
userId: string;
|
|
24
|
+
nickname: string;
|
|
25
|
+
content: MessageSegment[];
|
|
26
|
+
rawMessage: string;
|
|
27
|
+
timestampMs: number;
|
|
28
|
+
raw: IpcGuildMessageEventData;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class IcqqGuildCatalog {
|
|
32
|
+
private readonly channels = new Map<string, IcqqGuildChannelEntry>();
|
|
33
|
+
private readonly guilds = new Map<string, { guild_id: string; guild_name: string }>();
|
|
34
|
+
|
|
35
|
+
clear(): void {
|
|
36
|
+
this.channels.clear();
|
|
37
|
+
this.guilds.clear();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async syncAll(ipc: IpcClient): Promise<void> {
|
|
41
|
+
const listResp = await ipc.request(Actions.GUILD_LIST);
|
|
42
|
+
if (!listResp.ok) return;
|
|
43
|
+
|
|
44
|
+
const guildRows = Array.isArray(listResp.data) ? listResp.data : [];
|
|
45
|
+
for (const row of guildRows) {
|
|
46
|
+
const rec = row as Record<string, unknown>;
|
|
47
|
+
const guild_id = String(rec.guild_id ?? rec.id ?? "");
|
|
48
|
+
if (!guild_id) continue;
|
|
49
|
+
const guild_name = String(rec.guild_name ?? rec.name ?? guild_id);
|
|
50
|
+
this.guilds.set(guild_id, { guild_id, guild_name });
|
|
51
|
+
|
|
52
|
+
const chResp = await ipc.request(Actions.GUILD_CHANNELS, { guild_id });
|
|
53
|
+
if (!chResp.ok || !Array.isArray(chResp.data)) continue;
|
|
54
|
+
for (const ch of chResp.data) {
|
|
55
|
+
const chRec = ch as Record<string, unknown>;
|
|
56
|
+
const channel_id = String(chRec.channel_id ?? chRec.id ?? "");
|
|
57
|
+
if (!channel_id) continue;
|
|
58
|
+
const channel_name = String(chRec.channel_name ?? chRec.name ?? channel_id);
|
|
59
|
+
this.channels.set(channel_id, {
|
|
60
|
+
channel_id,
|
|
61
|
+
channel_name,
|
|
62
|
+
guild_id,
|
|
63
|
+
guild_name,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
upsertFromInbound(data: IpcGuildMessageEventData): void {
|
|
70
|
+
const guild_id = String(data.guild_id ?? "");
|
|
71
|
+
const channel_id = String(data.channel_id ?? "");
|
|
72
|
+
if (!guild_id || !channel_id) return;
|
|
73
|
+
const guild_name = String(data.guild_name ?? guild_id);
|
|
74
|
+
const channel_name = String(data.channel_name ?? channel_id);
|
|
75
|
+
this.guilds.set(guild_id, { guild_id, guild_name });
|
|
76
|
+
this.channels.set(channel_id, {
|
|
77
|
+
channel_id,
|
|
78
|
+
channel_name,
|
|
79
|
+
guild_id,
|
|
80
|
+
guild_name,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getGuildChannelList(): Array<{
|
|
85
|
+
id: string;
|
|
86
|
+
name: string;
|
|
87
|
+
parent: { type: "guild"; id: string; name: string };
|
|
88
|
+
}> {
|
|
89
|
+
return Array.from(this.channels.values()).map((entry) => ({
|
|
90
|
+
id: entry.channel_id,
|
|
91
|
+
name: entry.channel_name,
|
|
92
|
+
parent: {
|
|
93
|
+
type: "guild" as const,
|
|
94
|
+
id: entry.guild_id,
|
|
95
|
+
name: entry.guild_name,
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
resolveConsoleChannelNames(
|
|
101
|
+
channelId: string,
|
|
102
|
+
guildId?: string,
|
|
103
|
+
): { channelName?: string; parentName?: string } {
|
|
104
|
+
const entry = this.channels.get(channelId);
|
|
105
|
+
if (entry) {
|
|
106
|
+
return { channelName: entry.channel_name, parentName: entry.guild_name };
|
|
107
|
+
}
|
|
108
|
+
if (guildId) {
|
|
109
|
+
const guild = this.guilds.get(guildId);
|
|
110
|
+
return guild ? { parentName: guild.guild_name } : {};
|
|
111
|
+
}
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isIcqqGuildIpcEvent(eventName: string | undefined): boolean {
|
|
117
|
+
return typeof eventName === "string" && eventName.startsWith("message.guild");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function normalizeIcqqGuildInboundMessage(
|
|
121
|
+
data: IpcGuildMessageEventData & Record<string, unknown>,
|
|
122
|
+
): NormalizedIcqqGuildInbound | null {
|
|
123
|
+
if (data.type !== "guild") return null;
|
|
124
|
+
const guildId = String(data.guild_id ?? "");
|
|
125
|
+
const channelId = String(data.channel_id ?? "");
|
|
126
|
+
if (!guildId || !channelId) return null;
|
|
127
|
+
|
|
128
|
+
const userId = String(data.tiny_id ?? data.user_id ?? "");
|
|
129
|
+
const nickname = String(data.nickname ?? userId);
|
|
130
|
+
const rawMessage = String(data.raw_message ?? "");
|
|
131
|
+
const timestampMs =
|
|
132
|
+
typeof data.time === "number" ? data.time * 1000 : Date.now();
|
|
133
|
+
const messageId =
|
|
134
|
+
data.message_id != null && String(data.message_id) !== ""
|
|
135
|
+
? String(data.message_id)
|
|
136
|
+
: data.seq != null
|
|
137
|
+
? String(data.seq)
|
|
138
|
+
: `${data.time ?? 0}_${userId}_${channelId}`;
|
|
139
|
+
|
|
140
|
+
const parsed = parseCqMessage(rawMessage);
|
|
141
|
+
const canonical = toCanonicalSegments(parsed);
|
|
142
|
+
const content: MessageSegment[] = canonical.length > 0 ? canonical : [{ type: "text", data: { text: rawMessage } }];
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
messageId,
|
|
146
|
+
channelId,
|
|
147
|
+
guildId,
|
|
148
|
+
guildName: String(data.guild_name ?? guildId),
|
|
149
|
+
channelName: String(data.channel_name ?? channelId),
|
|
150
|
+
userId,
|
|
151
|
+
nickname,
|
|
152
|
+
content,
|
|
153
|
+
rawMessage,
|
|
154
|
+
timestampMs,
|
|
155
|
+
raw: data,
|
|
156
|
+
};
|
|
157
|
+
}
|
package/src/icqq-inbound.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { Message, type MessageSegment, type QuotedMessagePayload } from "zhin.js";
|
|
5
5
|
import { parseCqMessage } from "./cq-message.js";
|
|
6
|
+
import { toCanonicalSegments } from "./segment-mapper.js";
|
|
6
7
|
import { extractForwardResidFromJsonElement } from "./forward-msg.js";
|
|
7
8
|
import type { GroupRole } from "./types.js";
|
|
8
9
|
|
|
@@ -292,12 +293,22 @@ export function icqqElementsToSegments(
|
|
|
292
293
|
out.push({ type: "text", data: { text: String(el.text) } });
|
|
293
294
|
}
|
|
294
295
|
break;
|
|
295
|
-
case "at":
|
|
296
|
+
case "at": {
|
|
297
|
+
const atId = String(el.qq ?? el.user_id ?? "");
|
|
298
|
+
const name =
|
|
299
|
+
typeof el.name === "string"
|
|
300
|
+
? el.name
|
|
301
|
+
: typeof el.nickname === "string"
|
|
302
|
+
? el.nickname
|
|
303
|
+
: undefined;
|
|
296
304
|
out.push({
|
|
297
305
|
type: "at",
|
|
298
|
-
data:
|
|
306
|
+
data: name
|
|
307
|
+
? { qq: atId, user_id: atId, id: atId, name }
|
|
308
|
+
: { qq: atId, user_id: atId, id: atId },
|
|
299
309
|
});
|
|
300
310
|
break;
|
|
311
|
+
}
|
|
301
312
|
case "face":
|
|
302
313
|
out.push({ type: "face", data: { id: Number(el.id) } });
|
|
303
314
|
break;
|
|
@@ -584,7 +595,7 @@ function mergeReplyFromRawMessage(
|
|
|
584
595
|
const quoteFromRaw = Message.quoteIdFromContent(fromRaw);
|
|
585
596
|
if (!quoteFromRaw) return content;
|
|
586
597
|
return [
|
|
587
|
-
{ type: "reply", data: {
|
|
598
|
+
{ type: "reply", data: { message_id: quoteFromRaw } },
|
|
588
599
|
...content.filter((s) => s.type !== "reply"),
|
|
589
600
|
];
|
|
590
601
|
}
|
|
@@ -599,7 +610,7 @@ function mergeReplyFromSource(
|
|
|
599
610
|
resolveQuoteIdFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
600
611
|
if (!quoteId) return content;
|
|
601
612
|
return [
|
|
602
|
-
{ type: "reply", data: {
|
|
613
|
+
{ type: "reply", data: { message_id: quoteId } },
|
|
603
614
|
...content.filter((s) => s.type !== "reply"),
|
|
604
615
|
];
|
|
605
616
|
}
|
|
@@ -617,7 +628,7 @@ export function resolveInboundContent(
|
|
|
617
628
|
} else {
|
|
618
629
|
content = [{ type: "text", data: { text: "" } }];
|
|
619
630
|
}
|
|
620
|
-
return mergeReplyFromSource(content, data);
|
|
631
|
+
return toCanonicalSegments(mergeReplyFromSource(content, data));
|
|
621
632
|
}
|
|
622
633
|
|
|
623
634
|
export function normalizeIcqqInboundMessage(
|