@zhin.js/adapter-icqq 6.0.0 → 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 +19 -0
- package/README.md +2 -2
- package/dist/index.js +2 -2
- package/lib/adapter.d.ts +11 -4
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +13 -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 +110 -44
- 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.map +1 -1
- package/lib/icqq-inbound.js +15 -5
- package/lib/icqq-inbound.js.map +1 -1
- package/lib/index.d.ts +0 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +0 -1
- package/lib/index.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 +13 -6
- package/src/cq-message.ts +11 -3
- package/src/endpoint.ts +139 -51
- package/src/icqq-guild.ts +157 -0
- package/src/icqq-inbound.ts +16 -5
- package/src/index.ts +0 -8
- 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 -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/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, type QuotedMessagePayload,} from 'zhin.js';
|
|
8
8
|
import type {
|
|
9
9
|
IcqqEndpointConfig,
|
|
10
10
|
IcqqSenderInfo,
|
|
@@ -56,9 +56,12 @@ import {
|
|
|
56
56
|
import { parseIcqqGetMsgResponse } from "./get-msg.js";
|
|
57
57
|
import { enrichQuotedPayloadWithForward, isForwardPlaceholderPayload } from "./forward-msg.js";
|
|
58
58
|
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
IcqqGuildCatalog,
|
|
60
|
+
isIcqqGuildIpcEvent,
|
|
61
|
+
normalizeIcqqGuildInboundMessage,
|
|
62
|
+
type NormalizedIcqqGuildInbound,
|
|
63
|
+
} from "./icqq-guild.js";
|
|
64
|
+
import type { IpcGuildMessageEventData } from "./protocol.js";
|
|
62
65
|
|
|
63
66
|
export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessageEvent> {
|
|
64
67
|
$connected = false;
|
|
@@ -69,6 +72,8 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
69
72
|
friends = new Map<number, IpcFriendInfo>();
|
|
70
73
|
/** 缓存的群列表 */
|
|
71
74
|
groups = new Map<number, IpcGroupInfo>();
|
|
75
|
+
/** QQ 频道(guild)子频道缓存 */
|
|
76
|
+
readonly guildCatalog = new IcqqGuildCatalog();
|
|
72
77
|
|
|
73
78
|
private subscriptions: Array<{ unsubscribe: () => Promise<void> }> = [];
|
|
74
79
|
/** 事件去重:覆盖多端回流/服务端重复推送等场景 */
|
|
@@ -80,9 +85,6 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
80
85
|
/** 是否已有重连循环在跑(避免多次 schedule 叠套) */
|
|
81
86
|
private reconnectRunning = false;
|
|
82
87
|
|
|
83
|
-
/** Typing Indicator 管理器 */
|
|
84
|
-
$typingIndicator?: ICQQTypingIndicatorManager;
|
|
85
|
-
|
|
86
88
|
get $id() {
|
|
87
89
|
return this.$config.name;
|
|
88
90
|
}
|
|
@@ -113,50 +115,13 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
113
115
|
|
|
114
116
|
await this.rebindIpcSession();
|
|
115
117
|
|
|
116
|
-
// 根据配置自动启用 Typing Indicator
|
|
117
|
-
this.initTypingIndicator();
|
|
118
|
-
|
|
119
118
|
this.logger.info(formatCompact( {
|
|
120
119
|
机器人: this.$id,
|
|
121
120
|
好友数: this.friends.size,
|
|
122
121
|
群组数: this.groups.size,
|
|
123
|
-
思考提示: this.$typingIndicator ? '已启用' : '未启用',
|
|
124
122
|
}));
|
|
125
123
|
}
|
|
126
124
|
|
|
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
125
|
/** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
|
|
161
126
|
private async rebindIpcSession(): Promise<void> {
|
|
162
127
|
const uin = Number(this.$config.name);
|
|
@@ -268,6 +233,27 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
268
233
|
this.groups.set(g.group_id, g);
|
|
269
234
|
}
|
|
270
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);
|
|
271
257
|
}
|
|
272
258
|
|
|
273
259
|
// ── 断开 ───────────────────────────────────────────────────────────
|
|
@@ -275,12 +261,6 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
275
261
|
async $disconnect(): Promise<void> {
|
|
276
262
|
this.intentionalDisconnect = true;
|
|
277
263
|
|
|
278
|
-
// 停止所有 Typing Indicator
|
|
279
|
-
if (this.$typingIndicator) {
|
|
280
|
-
await this.$typingIndicator.stopAll().catch(() => {});
|
|
281
|
-
this.$typingIndicator = undefined;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
264
|
this.ipc?.setOnRemoteDisconnect(null);
|
|
285
265
|
for (const sub of this.subscriptions) {
|
|
286
266
|
await sub.unsubscribe().catch(() => {});
|
|
@@ -301,6 +281,23 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
301
281
|
return;
|
|
302
282
|
}
|
|
303
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
|
+
|
|
304
301
|
const payload = unwrapIcqqIpcEventPayload(event);
|
|
305
302
|
if (!payload || typeof payload !== "object") {
|
|
306
303
|
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
@@ -393,6 +390,80 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
393
390
|
);
|
|
394
391
|
}
|
|
395
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
|
+
|
|
396
467
|
private handleNoticeEvent(event: IcqqIpcRawEvent): void {
|
|
397
468
|
const dedupeKey = resolveSideEventDedupeKey(event, "notice");
|
|
398
469
|
if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
|
|
@@ -657,7 +728,8 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
657
728
|
|
|
658
729
|
async $sendMessage(options: SendOptions): Promise<string> {
|
|
659
730
|
const outboundMedia = resolveIcqqOutboundMediaMode(this.$config);
|
|
660
|
-
const
|
|
731
|
+
const expanded = expandInteractiveSegmentsInContent(options.content);
|
|
732
|
+
const qrcodeContent = expanded;
|
|
661
733
|
const content = materializeOutboundBase64(qrcodeContent, outboundMedia);
|
|
662
734
|
const message = buildIcqqIpcMessageImpl(content);
|
|
663
735
|
|
|
@@ -685,6 +757,22 @@ export class IcqqEndpoint implements Endpoint<IcqqEndpointConfig, IcqqIpcMessage
|
|
|
685
757
|
action = Actions.SEND_GROUP_MSG;
|
|
686
758
|
params = { group_id: Number(options.id), message };
|
|
687
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
|
+
}
|
|
688
776
|
default:
|
|
689
777
|
throw new Error(`不支持的频道类型: ${options.type}`);
|
|
690
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
|
|
|
@@ -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(
|
package/src/index.ts
CHANGED
|
@@ -31,14 +31,6 @@ declare module "zhin.js" {
|
|
|
31
31
|
export * from "./types.js";
|
|
32
32
|
export { IcqqEndpoint } from "./endpoint.js";
|
|
33
33
|
export { IcqqAdapter } from "./adapter.js";
|
|
34
|
-
export {
|
|
35
|
-
ICQQTypingIndicatorManager,
|
|
36
|
-
createICQQTypingIndicatorManager,
|
|
37
|
-
startICQQTypingIndicator,
|
|
38
|
-
enableTypingIndicator,
|
|
39
|
-
type ICQQTypingIndicatorConfig,
|
|
40
|
-
} from "./typing-indicator.js";
|
|
41
|
-
export type { TypingIndicatorConfig } from "./types.js";
|
|
42
34
|
|
|
43
35
|
const plugin = usePlugin();
|
|
44
36
|
const { provide, useContext, addCommand, root } = plugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { toCanonicalSegments, fromCanonicalSegments } from 'zhin.js';
|
package/src/tools/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ICQQ 平台特有工具注册 — 通过 IPC 调用守护进程 Actions
|
|
3
3
|
*
|
|
4
|
-
* 通用群管理工具由
|
|
4
|
+
* 通用群管理工具由 createSceneManagementTools() 自动生成;
|
|
5
5
|
* 本模块注册 ICQQ 独有的扩展工具。
|
|
6
6
|
*/
|
|
7
7
|
import type { ToolFeature } from "zhin.js";
|
|
8
|
-
import {
|
|
8
|
+
import { createSceneManagementTools } from "zhin.js";
|
|
9
9
|
import type { IcqqAdapter } from "../adapter.js";
|
|
10
10
|
import { Actions } from "../protocol.js";
|
|
11
11
|
|
|
@@ -17,8 +17,8 @@ export function registerTools(
|
|
|
17
17
|
const disposers: (() => void)[] = [];
|
|
18
18
|
|
|
19
19
|
// ── 通用群管工具 ───────────────────────────────────────────────────
|
|
20
|
-
const
|
|
21
|
-
disposers.push(...
|
|
20
|
+
const sceneTools = createSceneManagementTools(icqq, "icqq");
|
|
21
|
+
disposers.push(...sceneTools.map((t) => toolService.addTool(t, pluginName)));
|
|
22
22
|
|
|
23
23
|
// ── 设置头衔 ───────────────────────────────────────────────────────
|
|
24
24
|
disposers.push(
|
|
@@ -39,7 +39,7 @@ export function registerTools(
|
|
|
39
39
|
},
|
|
40
40
|
platforms: ["icqq"],
|
|
41
41
|
tags: ["icqq"],
|
|
42
|
-
permissions: ["platform(icqq,
|
|
42
|
+
permissions: ["platform(icqq,scene_owner)"],
|
|
43
43
|
execute: async (args: Record<string, any>) => {
|
|
44
44
|
const endpoint = icqq.endpoints.get(args.endpoint_id);
|
|
45
45
|
if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
@@ -73,7 +73,7 @@ export function registerTools(
|
|
|
73
73
|
},
|
|
74
74
|
platforms: ["icqq"],
|
|
75
75
|
tags: ["icqq"],
|
|
76
|
-
permissions: ["platform(icqq,
|
|
76
|
+
permissions: ["platform(icqq,scene_admin)"],
|
|
77
77
|
execute: async (args: Record<string, any>) => {
|
|
78
78
|
const endpoint = icqq.endpoints.get(args.endpoint_id);
|
|
79
79
|
if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
@@ -135,7 +135,7 @@ export function registerTools(
|
|
|
135
135
|
},
|
|
136
136
|
platforms: ["icqq"],
|
|
137
137
|
tags: ["icqq"],
|
|
138
|
-
permissions: ["platform(icqq,
|
|
138
|
+
permissions: ["platform(icqq,scene_admin)"],
|
|
139
139
|
execute: async (args: Record<string, any>) => {
|
|
140
140
|
const endpoint = icqq.endpoints.get(args.endpoint_id);
|
|
141
141
|
if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
@@ -199,7 +199,7 @@ export function registerTools(
|
|
|
199
199
|
},
|
|
200
200
|
platforms: ["icqq"],
|
|
201
201
|
tags: ["icqq"],
|
|
202
|
-
permissions: ["platform(icqq,
|
|
202
|
+
permissions: ["platform(icqq,scene_owner)"],
|
|
203
203
|
execute: async (args: Record<string, any>) => {
|
|
204
204
|
const endpoint = icqq.endpoints.get(args.endpoint_id);
|
|
205
205
|
if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
|
@@ -393,7 +393,7 @@ export function registerTools(
|
|
|
393
393
|
},
|
|
394
394
|
platforms: ["icqq"],
|
|
395
395
|
tags: ["icqq"],
|
|
396
|
-
permissions: ["platform(icqq,
|
|
396
|
+
permissions: ["platform(icqq,scene_admin)"],
|
|
397
397
|
execute: async (args: Record<string, any>) => {
|
|
398
398
|
const endpoint = icqq.endpoints.get(args.endpoint_id);
|
|
399
399
|
if (!endpoint) throw new Error(`Endpoint ${args.endpoint_id} 不存在`);
|
package/src/types.ts
CHANGED
|
@@ -17,46 +17,6 @@ export interface IcqqSenderInfo {
|
|
|
17
17
|
title?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Typing Indicator 配置
|
|
22
|
-
*/
|
|
23
|
-
export interface TypingIndicatorConfig {
|
|
24
|
-
/** 是否启用(默认 true) */
|
|
25
|
-
enabled?: boolean;
|
|
26
|
-
/** 默认表情(默认 '⏳') */
|
|
27
|
-
defaultEmoji?: string;
|
|
28
|
-
/** 是否在完成后自动移除(默认 true) */
|
|
29
|
-
autoRemove?: boolean;
|
|
30
|
-
/** 自动移除延迟,毫秒(默认 5000) */
|
|
31
|
-
removeDelay?: number;
|
|
32
|
-
/** 私聊配置 */
|
|
33
|
-
privateConfig?: {
|
|
34
|
-
/** 提示类型 */
|
|
35
|
-
type: 'message' | 'reaction';
|
|
36
|
-
/** 消息内容(type='message' 时使用) */
|
|
37
|
-
message?: string;
|
|
38
|
-
/** 表情符号(type='reaction' 时使用) */
|
|
39
|
-
emoji?: string;
|
|
40
|
-
/** 是否自动移除 */
|
|
41
|
-
autoRemove?: boolean;
|
|
42
|
-
/** 自动移除延迟 */
|
|
43
|
-
removeDelay?: number;
|
|
44
|
-
};
|
|
45
|
-
/** 群聊配置 */
|
|
46
|
-
groupConfig?: {
|
|
47
|
-
/** 提示类型 */
|
|
48
|
-
type: 'message' | 'reaction';
|
|
49
|
-
/** 消息内容(type='message' 时使用) */
|
|
50
|
-
message?: string;
|
|
51
|
-
/** 表情符号(type='reaction' 时使用) */
|
|
52
|
-
emoji?: string;
|
|
53
|
-
/** 是否自动移除 */
|
|
54
|
-
autoRemove?: boolean;
|
|
55
|
-
/** 自动移除延迟 */
|
|
56
|
-
removeDelay?: number;
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
20
|
/**
|
|
61
21
|
* Endpoint 配置:支持本地 IPC 和远程 RPC 两种连接模式。
|
|
62
22
|
*
|
|
@@ -81,11 +41,6 @@ export interface IcqqEndpointConfig {
|
|
|
81
41
|
* 默认 true;设为 false 则断开后仅将 `$connected` 置为 false,需手动重连。
|
|
82
42
|
*/
|
|
83
43
|
autoReconnect?: boolean;
|
|
84
|
-
/**
|
|
85
|
-
* Typing Indicator 配置
|
|
86
|
-
* 用于在 AI 处理消息时提示用户正在处理中
|
|
87
|
-
*/
|
|
88
|
-
typingIndicator?: TypingIndicatorConfig;
|
|
89
44
|
/**
|
|
90
45
|
* 出站图片/语音/视频:file=本机临时文件路径(需与 icqq 守护进程同机可读);
|
|
91
46
|
* base64=经 CQ `base64://` 交给守护进程解码(Zhin 与 icqq 异进程/异机时用,配置 rpc 时默认 base64)。
|