@zhin.js/adapter-icqq 5.0.2 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +32 -4
- package/client/LoginAssistPanel.tsx +38 -3
- package/dist/index.js +2 -2
- package/lib/adapter.d.ts +12 -4
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +14 -6
- package/lib/adapter.js.map +1 -1
- package/lib/cq-message.d.ts.map +1 -1
- package/lib/cq-message.js +12 -3
- package/lib/cq-message.js.map +1 -1
- package/lib/endpoint.d.ts +12 -8
- package/lib/endpoint.d.ts.map +1 -1
- package/lib/endpoint.js +135 -47
- package/lib/endpoint.js.map +1 -1
- package/lib/icqq-guild.d.ts +48 -0
- package/lib/icqq-guild.d.ts.map +1 -0
- package/lib/icqq-guild.js +115 -0
- package/lib/icqq-guild.js.map +1 -0
- package/lib/icqq-inbound.d.ts +7 -0
- package/lib/icqq-inbound.d.ts.map +1 -1
- package/lib/icqq-inbound.js +39 -6
- package/lib/icqq-inbound.js.map +1 -1
- package/lib/icqq-login-assist-bridge.d.ts +5 -0
- package/lib/icqq-login-assist-bridge.d.ts.map +1 -0
- package/lib/icqq-login-assist-bridge.js +148 -0
- package/lib/icqq-login-assist-bridge.js.map +1 -0
- package/lib/index.d.ts +0 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/login-assist-routes.d.ts +7 -0
- package/lib/login-assist-routes.d.ts.map +1 -0
- package/lib/login-assist-routes.js +29 -0
- package/lib/login-assist-routes.js.map +1 -0
- package/lib/login-ipc-contract.d.ts +44 -0
- package/lib/login-ipc-contract.d.ts.map +1 -0
- package/lib/login-ipc-contract.js +35 -0
- package/lib/login-ipc-contract.js.map +1 -0
- package/lib/protocol.d.ts +32 -1
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +23 -0
- package/lib/protocol.js.map +1 -1
- package/lib/segment-mapper.d.ts +2 -0
- package/lib/segment-mapper.d.ts.map +1 -0
- package/lib/segment-mapper.js +2 -0
- package/lib/segment-mapper.js.map +1 -0
- package/lib/tools/index.d.ts +1 -1
- package/lib/tools/index.js +8 -8
- package/lib/types.d.ts +0 -44
- package/lib/types.d.ts.map +1 -1
- package/package.json +18 -18
- package/skills/icqq/PERMITS.md +6 -6
- package/src/adapter.ts +14 -6
- package/src/cq-message.ts +11 -3
- package/src/endpoint.ts +169 -52
- package/src/icqq-guild.ts +157 -0
- package/src/icqq-inbound.ts +52 -7
- package/src/icqq-login-assist-bridge.ts +196 -0
- package/src/index.ts +2 -8
- package/src/login-assist-routes.ts +39 -0
- package/src/login-ipc-contract.ts +52 -0
- package/src/protocol.ts +32 -1
- package/src/segment-mapper.ts +1 -0
- package/src/tools/index.ts +9 -9
- package/src/types.ts +0 -45
- 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 -225
- package/lib/typing-indicator.js.map +0 -1
- package/src/typing-indicator-example.ts +0 -269
- package/src/typing-indicator.ts +0 -313
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
|
|
7
|
+
import { formatCompact, Endpoint, Message, segment, SendContent, SendOptions, expandInteractiveSegmentsInContent, type QuotedMessagePayload,} from 'zhin.js';
|
|
8
8
|
import type {
|
|
9
9
|
IcqqEndpointConfig,
|
|
10
10
|
IcqqSenderInfo,
|
|
@@ -49,9 +49,19 @@ import {
|
|
|
49
49
|
shouldRefreshListsOnMeta,
|
|
50
50
|
type IcqqIpcRawEvent,
|
|
51
51
|
} from "./icqq-side-events.js";
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
disposeIcqqLoginAssistBridge,
|
|
54
|
+
handleIcqqLoginIpcEvent,
|
|
55
|
+
} from "./icqq-login-assist-bridge.js";
|
|
53
56
|
import { parseIcqqGetMsgResponse } from "./get-msg.js";
|
|
54
57
|
import { enrichQuotedPayloadWithForward, isForwardPlaceholderPayload } from "./forward-msg.js";
|
|
58
|
+
import {
|
|
59
|
+
IcqqGuildCatalog,
|
|
60
|
+
isIcqqGuildIpcEvent,
|
|
61
|
+
normalizeIcqqGuildInboundMessage,
|
|
62
|
+
type NormalizedIcqqGuildInbound,
|
|
63
|
+
} from "./icqq-guild.js";
|
|
64
|
+
import type { IpcGuildMessageEventData } from "./protocol.js";
|
|
55
65
|
|
|
56
66
|
export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessageEvent> {
|
|
57
67
|
$connected = false;
|
|
@@ -62,6 +72,8 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
62
72
|
friends = new Map<number, IpcFriendInfo>();
|
|
63
73
|
/** 缓存的群列表 */
|
|
64
74
|
groups = new Map<number, IpcGroupInfo>();
|
|
75
|
+
/** QQ 频道(guild)子频道缓存 */
|
|
76
|
+
readonly guildCatalog = new IcqqGuildCatalog();
|
|
65
77
|
|
|
66
78
|
private subscriptions: Array<{ unsubscribe: () => Promise<void> }> = [];
|
|
67
79
|
/** 事件去重:覆盖多端回流/服务端重复推送等场景 */
|
|
@@ -73,9 +85,6 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
73
85
|
/** 是否已有重连循环在跑(避免多次 schedule 叠套) */
|
|
74
86
|
private reconnectRunning = false;
|
|
75
87
|
|
|
76
|
-
/** Typing Indicator 管理器 */
|
|
77
|
-
$typingIndicator?: ICQQTypingIndicatorManager;
|
|
78
|
-
|
|
79
88
|
get $id() {
|
|
80
89
|
return this.$config.name;
|
|
81
90
|
}
|
|
@@ -106,50 +115,13 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
106
115
|
|
|
107
116
|
await this.rebindIpcSession();
|
|
108
117
|
|
|
109
|
-
// 根据配置自动启用 Typing Indicator
|
|
110
|
-
this.initTypingIndicator();
|
|
111
|
-
|
|
112
118
|
this.logger.info(formatCompact( {
|
|
113
119
|
机器人: this.$id,
|
|
114
120
|
好友数: this.friends.size,
|
|
115
121
|
群组数: this.groups.size,
|
|
116
|
-
思考提示: this.$typingIndicator ? '已启用' : '未启用',
|
|
117
122
|
}));
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
/**
|
|
121
|
-
* 初始化 Typing Indicator
|
|
122
|
-
* 根据配置自动启用或禁用
|
|
123
|
-
*/
|
|
124
|
-
private initTypingIndicator(): void {
|
|
125
|
-
const typingConfig = this.$config.typingIndicator;
|
|
126
|
-
|
|
127
|
-
// 如果配置中明确设置为 false,则不启用
|
|
128
|
-
if (typingConfig?.enabled === false) {
|
|
129
|
-
this.logger.debug(formatCompact({
|
|
130
|
-
endpoint: this.$id,
|
|
131
|
-
typingIndicator: 'disabled_by_config',
|
|
132
|
-
}));
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 启用 Typing Indicator
|
|
137
|
-
try {
|
|
138
|
-
this.$typingIndicator = enableTypingIndicator(this, typingConfig);
|
|
139
|
-
this.logger.debug(formatCompact({
|
|
140
|
-
endpoint: this.$id,
|
|
141
|
-
typingIndicator: 'enabled',
|
|
142
|
-
emoji: typingConfig?.defaultEmoji || '⏳',
|
|
143
|
-
}));
|
|
144
|
-
} catch (error) {
|
|
145
|
-
this.logger.warn(formatCompact({
|
|
146
|
-
endpoint: this.$id,
|
|
147
|
-
typingIndicator: 'failed',
|
|
148
|
-
error: error instanceof Error ? error.message : String(error),
|
|
149
|
-
}));
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
125
|
/** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
|
|
154
126
|
private async rebindIpcSession(): Promise<void> {
|
|
155
127
|
const uin = Number(this.$config.name);
|
|
@@ -261,6 +233,27 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
261
233
|
this.groups.set(g.group_id, g);
|
|
262
234
|
}
|
|
263
235
|
}
|
|
236
|
+
|
|
237
|
+
await this.guildCatalog.syncAll(this.ipc).catch((e: unknown) => {
|
|
238
|
+
this.logger.warn(formatCompact({
|
|
239
|
+
op: "sync_guilds",
|
|
240
|
+
endpoint: this.$id,
|
|
241
|
+
ok: false,
|
|
242
|
+
error: e instanceof Error ? e.message : String(e),
|
|
243
|
+
}));
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Console:列出 guild 子频道(含 parent.guild) */
|
|
248
|
+
getGuildChannelList(): ReturnType<IcqqGuildCatalog["getGuildChannelList"]> {
|
|
249
|
+
return this.guildCatalog.getGuildChannelList();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
resolveConsoleChannelNames(
|
|
253
|
+
channelId: string,
|
|
254
|
+
guildId?: string,
|
|
255
|
+
): { channelName?: string; parentName?: string } {
|
|
256
|
+
return this.guildCatalog.resolveConsoleChannelNames(channelId, guildId);
|
|
264
257
|
}
|
|
265
258
|
|
|
266
259
|
// ── 断开 ───────────────────────────────────────────────────────────
|
|
@@ -268,18 +261,13 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
268
261
|
async $disconnect(): Promise<void> {
|
|
269
262
|
this.intentionalDisconnect = true;
|
|
270
263
|
|
|
271
|
-
// 停止所有 Typing Indicator
|
|
272
|
-
if (this.$typingIndicator) {
|
|
273
|
-
await this.$typingIndicator.stopAll().catch(() => {});
|
|
274
|
-
this.$typingIndicator = undefined;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
264
|
this.ipc?.setOnRemoteDisconnect(null);
|
|
278
265
|
for (const sub of this.subscriptions) {
|
|
279
266
|
await sub.unsubscribe().catch(() => {});
|
|
280
267
|
}
|
|
281
268
|
this.subscriptions = [];
|
|
282
269
|
this.inboundDeduper.clear();
|
|
270
|
+
disposeIcqqLoginAssistBridge(this.$id);
|
|
283
271
|
this.ipc?.close();
|
|
284
272
|
this.$connected = false;
|
|
285
273
|
this.logger.info(formatCompact( { op: "disconnect", endpoint: this.$id }));
|
|
@@ -288,6 +276,28 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
288
276
|
// ── 消息处理 ───────────────────────────────────────────────────────
|
|
289
277
|
|
|
290
278
|
private handleEvent(event: IpcEvent): void {
|
|
279
|
+
if (event.event?.startsWith("system.login.")) {
|
|
280
|
+
handleIcqqLoginIpcEvent(this, event.event, event.data);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (event.event === Actions.RELOAD_GUILDS) {
|
|
285
|
+
void this.guildCatalog.syncAll(this.ipc).catch((e: unknown) => {
|
|
286
|
+
this.logger.warn(formatCompact({
|
|
287
|
+
op: "reload_guilds",
|
|
288
|
+
endpoint: this.$id,
|
|
289
|
+
ok: false,
|
|
290
|
+
error: e instanceof Error ? e.message : String(e),
|
|
291
|
+
}));
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (isIcqqGuildIpcEvent(event.event)) {
|
|
297
|
+
this.handleGuildMessageEvent(event);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
291
301
|
const payload = unwrapIcqqIpcEventPayload(event);
|
|
292
302
|
if (!payload || typeof payload !== "object") {
|
|
293
303
|
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
@@ -380,6 +390,80 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
380
390
|
);
|
|
381
391
|
}
|
|
382
392
|
|
|
393
|
+
private handleGuildMessageEvent(event: IpcEvent): void {
|
|
394
|
+
const payload = event.data;
|
|
395
|
+
if (!payload || typeof payload !== "object") return;
|
|
396
|
+
const data = payload as IpcGuildMessageEventData & Record<string, unknown>;
|
|
397
|
+
const normalized = normalizeIcqqGuildInboundMessage(data);
|
|
398
|
+
if (!normalized) return;
|
|
399
|
+
|
|
400
|
+
this.guildCatalog.upsertFromInbound(data);
|
|
401
|
+
|
|
402
|
+
if (!this.inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
void this.dispatchGuildInboundMessage(normalized);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async dispatchGuildInboundMessage(
|
|
410
|
+
normalized: NormalizedIcqqGuildInbound,
|
|
411
|
+
): Promise<void> {
|
|
412
|
+
const message = this.$formatGuildMessage(normalized);
|
|
413
|
+
this.adapter.emit("message.receive", message);
|
|
414
|
+
this.logger.debug(
|
|
415
|
+
`${this.$id} recv channel(${message.$channel.id}):${segment.raw(message.$content)}`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private $formatGuildMessage(
|
|
420
|
+
normalized: NormalizedIcqqGuildInbound,
|
|
421
|
+
): ReturnType<typeof Message.from<IpcGuildMessageEventData>> {
|
|
422
|
+
const senderInfo: IcqqSenderInfo = {
|
|
423
|
+
id: normalized.userId,
|
|
424
|
+
name: normalized.nickname,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const result = Message.from(normalized.raw, {
|
|
428
|
+
$id: normalized.messageId,
|
|
429
|
+
$adapter: "icqq" as const,
|
|
430
|
+
$endpoint: this.$config.name,
|
|
431
|
+
$sender: senderInfo,
|
|
432
|
+
$channel: {
|
|
433
|
+
id: normalized.channelId,
|
|
434
|
+
type: "channel",
|
|
435
|
+
parent: { type: "guild", id: normalized.guildId },
|
|
436
|
+
},
|
|
437
|
+
$content: normalized.content,
|
|
438
|
+
$raw: normalized.rawMessage,
|
|
439
|
+
$timestamp: normalized.timestampMs,
|
|
440
|
+
$recall: async () => {
|
|
441
|
+
await this.$recallMessage(result.$id);
|
|
442
|
+
},
|
|
443
|
+
$reply: async (
|
|
444
|
+
content: SendContent,
|
|
445
|
+
quote?: boolean | string,
|
|
446
|
+
): Promise<string> => {
|
|
447
|
+
if (!Array.isArray(content)) content = [content];
|
|
448
|
+
if (quote) {
|
|
449
|
+
content.unshift({
|
|
450
|
+
type: "reply",
|
|
451
|
+
data: { id: typeof quote === "boolean" ? result.$id : quote },
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return await this.adapter.sendMessage({
|
|
455
|
+
context: "icqq",
|
|
456
|
+
endpoint: this.$config.name,
|
|
457
|
+
id: normalized.channelId,
|
|
458
|
+
type: "channel",
|
|
459
|
+
parent: { type: "guild", id: normalized.guildId },
|
|
460
|
+
content,
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
|
|
383
467
|
private handleNoticeEvent(event: IcqqIpcRawEvent): void {
|
|
384
468
|
const dedupeKey = resolveSideEventDedupeKey(event, "notice");
|
|
385
469
|
if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
|
|
@@ -503,6 +587,9 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
503
587
|
$channel: {
|
|
504
588
|
id: normalized.channelId,
|
|
505
589
|
type: normalized.channelType,
|
|
590
|
+
...(normalized.channelParentGroupId
|
|
591
|
+
? { parent: { type: 'group' as const, id: normalized.channelParentGroupId } }
|
|
592
|
+
: {}),
|
|
506
593
|
},
|
|
507
594
|
$content: normalized.content,
|
|
508
595
|
$quote_id: quoteId,
|
|
@@ -641,21 +728,51 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
641
728
|
|
|
642
729
|
async $sendMessage(options: SendOptions): Promise<string> {
|
|
643
730
|
const outboundMedia = resolveIcqqOutboundMediaMode(this.$config);
|
|
644
|
-
const
|
|
731
|
+
const expanded = expandInteractiveSegmentsInContent(options.content);
|
|
732
|
+
const qrcodeContent = expanded;
|
|
733
|
+
const content = materializeOutboundBase64(qrcodeContent, outboundMedia);
|
|
645
734
|
const message = buildIcqqIpcMessageImpl(content);
|
|
646
735
|
|
|
647
736
|
let action: string;
|
|
648
737
|
let params: Record<string, unknown>;
|
|
649
738
|
|
|
650
739
|
switch (options.type) {
|
|
651
|
-
case "private":
|
|
652
|
-
|
|
653
|
-
|
|
740
|
+
case "private": {
|
|
741
|
+
const groupParent =
|
|
742
|
+
options.parent?.type === "group" ? options.parent.id : undefined;
|
|
743
|
+
if (groupParent) {
|
|
744
|
+
action = Actions.SEND_TEMP_MSG;
|
|
745
|
+
params = {
|
|
746
|
+
group_id: Number(groupParent),
|
|
747
|
+
user_id: Number(options.id),
|
|
748
|
+
message,
|
|
749
|
+
};
|
|
750
|
+
} else {
|
|
751
|
+
action = Actions.SEND_PRIVATE_MSG;
|
|
752
|
+
params = { user_id: Number(options.id), message };
|
|
753
|
+
}
|
|
654
754
|
break;
|
|
755
|
+
}
|
|
655
756
|
case "group":
|
|
656
757
|
action = Actions.SEND_GROUP_MSG;
|
|
657
758
|
params = { group_id: Number(options.id), message };
|
|
658
759
|
break;
|
|
760
|
+
case "channel": {
|
|
761
|
+
const guildId =
|
|
762
|
+
options.parent?.type === "guild" ? options.parent.id : undefined;
|
|
763
|
+
if (!guildId) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
'channel 发送需要 parent: { type: "guild", id: guild_id }',
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
action = Actions.GUILD_SEND_MSG;
|
|
769
|
+
params = {
|
|
770
|
+
guild_id: guildId,
|
|
771
|
+
channel_id: options.id,
|
|
772
|
+
message,
|
|
773
|
+
};
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
659
776
|
default:
|
|
660
777
|
throw new Error(`不支持的频道类型: ${options.type}`);
|
|
661
778
|
}
|
|
@@ -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
|
|
|
@@ -30,6 +31,8 @@ export interface IcqqIpcMessageEvent extends IcqqIpcEventBase {
|
|
|
30
31
|
user_uid?: string;
|
|
31
32
|
nickname?: string;
|
|
32
33
|
card?: string;
|
|
34
|
+
/** 群临时会话来源群号 */
|
|
35
|
+
group_id?: number;
|
|
33
36
|
/** NT/IPC 群成员角色:owner | admin | member */
|
|
34
37
|
role?: GroupRole | string;
|
|
35
38
|
};
|
|
@@ -81,6 +84,8 @@ export interface NormalizedIcqqInbound {
|
|
|
81
84
|
idSource: "message_id" | "msg_id" | "seq" | "synthetic";
|
|
82
85
|
channelType: "group" | "private";
|
|
83
86
|
channelId: string;
|
|
87
|
+
/** 群临时会话:来源群号 */
|
|
88
|
+
channelParentGroupId?: string;
|
|
84
89
|
userId: string;
|
|
85
90
|
nickname: string;
|
|
86
91
|
/** 群聊时来自 IPC sender.role(私聊无) */
|
|
@@ -204,9 +209,38 @@ export function shouldSkipSelfInboundMessage(
|
|
|
204
209
|
return Number(data.self_id) === Number(data.user_id);
|
|
205
210
|
}
|
|
206
211
|
|
|
212
|
+
/** 群临时会话私聊:icqq message_type=private & sub_type=group */
|
|
213
|
+
export function isIcqqGroupTempPrivateMessage(data: IcqqIpcMessageEvent): boolean {
|
|
214
|
+
const messageType =
|
|
215
|
+
data.message_type ??
|
|
216
|
+
(data.type === "group" || data.type === "private" ? data.type : undefined);
|
|
217
|
+
if (messageType !== "private") return false;
|
|
218
|
+
if (data.sub_type === "group") return true;
|
|
219
|
+
const groupId = data.sender?.group_id;
|
|
220
|
+
return typeof groupId === "number" && groupId > 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
207
223
|
export function resolveChannelFromIcqqMessage(
|
|
208
224
|
data: IcqqIpcMessageEvent,
|
|
209
|
-
): {
|
|
225
|
+
): {
|
|
226
|
+
channelType: "group" | "private";
|
|
227
|
+
channelId: string;
|
|
228
|
+
channelParentGroupId?: string;
|
|
229
|
+
} {
|
|
230
|
+
if (isIcqqGroupTempPrivateMessage(data)) {
|
|
231
|
+
const groupId = data.sender?.group_id ?? data.group_id;
|
|
232
|
+
const userId = resolveIcqqInboundUserId(
|
|
233
|
+
data as unknown as Record<string, unknown>,
|
|
234
|
+
);
|
|
235
|
+
if (groupId != null && userId != null) {
|
|
236
|
+
return {
|
|
237
|
+
channelType: "private",
|
|
238
|
+
channelId: String(userId),
|
|
239
|
+
channelParentGroupId: String(groupId),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
210
244
|
const messageType =
|
|
211
245
|
data.message_type ??
|
|
212
246
|
(data.type === "group" || data.type === "private" ? data.type : undefined);
|
|
@@ -259,12 +293,22 @@ export function icqqElementsToSegments(
|
|
|
259
293
|
out.push({ type: "text", data: { text: String(el.text) } });
|
|
260
294
|
}
|
|
261
295
|
break;
|
|
262
|
-
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;
|
|
263
304
|
out.push({
|
|
264
305
|
type: "at",
|
|
265
|
-
data:
|
|
306
|
+
data: name
|
|
307
|
+
? { qq: atId, user_id: atId, id: atId, name }
|
|
308
|
+
: { qq: atId, user_id: atId, id: atId },
|
|
266
309
|
});
|
|
267
310
|
break;
|
|
311
|
+
}
|
|
268
312
|
case "face":
|
|
269
313
|
out.push({ type: "face", data: { id: Number(el.id) } });
|
|
270
314
|
break;
|
|
@@ -551,7 +595,7 @@ function mergeReplyFromRawMessage(
|
|
|
551
595
|
const quoteFromRaw = Message.quoteIdFromContent(fromRaw);
|
|
552
596
|
if (!quoteFromRaw) return content;
|
|
553
597
|
return [
|
|
554
|
-
{ type: "reply", data: {
|
|
598
|
+
{ type: "reply", data: { message_id: quoteFromRaw } },
|
|
555
599
|
...content.filter((s) => s.type !== "reply"),
|
|
556
600
|
];
|
|
557
601
|
}
|
|
@@ -566,7 +610,7 @@ function mergeReplyFromSource(
|
|
|
566
610
|
resolveQuoteIdFromIcqqSource(findIcqqNestedMessageSource(data));
|
|
567
611
|
if (!quoteId) return content;
|
|
568
612
|
return [
|
|
569
|
-
{ type: "reply", data: {
|
|
613
|
+
{ type: "reply", data: { message_id: quoteId } },
|
|
570
614
|
...content.filter((s) => s.type !== "reply"),
|
|
571
615
|
];
|
|
572
616
|
}
|
|
@@ -584,7 +628,7 @@ export function resolveInboundContent(
|
|
|
584
628
|
} else {
|
|
585
629
|
content = [{ type: "text", data: { text: "" } }];
|
|
586
630
|
}
|
|
587
|
-
return mergeReplyFromSource(content, data);
|
|
631
|
+
return toCanonicalSegments(mergeReplyFromSource(content, data));
|
|
588
632
|
}
|
|
589
633
|
|
|
590
634
|
export function normalizeIcqqInboundMessage(
|
|
@@ -594,7 +638,7 @@ export function normalizeIcqqInboundMessage(
|
|
|
594
638
|
const ext = data as IcqqIpcMessageEvent & Record<string, unknown>;
|
|
595
639
|
const userIdNum = resolveIcqqInboundUserId(ext);
|
|
596
640
|
if (userIdNum == null) return null;
|
|
597
|
-
const { channelType, channelId } = resolveChannelFromIcqqMessage(data);
|
|
641
|
+
const { channelType, channelId, channelParentGroupId } = resolveChannelFromIcqqMessage(data);
|
|
598
642
|
const resolved = resolveIcqqInboundMessageId(data, channelId);
|
|
599
643
|
const nickname =
|
|
600
644
|
data.sender?.nickname ?? data.nickname ?? String(userIdNum);
|
|
@@ -608,6 +652,7 @@ export function normalizeIcqqInboundMessage(
|
|
|
608
652
|
idSource: resolved.source,
|
|
609
653
|
channelType,
|
|
610
654
|
channelId,
|
|
655
|
+
channelParentGroupId,
|
|
611
656
|
userId: String(userIdNum),
|
|
612
657
|
nickname,
|
|
613
658
|
senderRole,
|