@zhin.js/adapter-icqq 3.0.4 → 3.0.6
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 +106 -78
- package/README.md +3 -3
- package/client/index.tsx +1 -1
- package/client/tsconfig.json +1 -1
- package/dist/index.js +1 -1
- package/lib/bot.d.ts +56 -12
- package/lib/bot.d.ts.map +1 -1
- package/lib/bot.js +416 -136
- package/lib/bot.js.map +1 -1
- package/lib/cq-message.d.ts +10 -0
- package/lib/cq-message.d.ts.map +1 -0
- package/lib/cq-message.js +119 -0
- package/lib/cq-message.js.map +1 -0
- package/lib/forward-msg.d.ts +27 -0
- package/lib/forward-msg.d.ts.map +1 -0
- package/lib/forward-msg.js +387 -0
- package/lib/forward-msg.js.map +1 -0
- package/lib/get-msg.d.ts +3 -0
- package/lib/get-msg.d.ts.map +1 -0
- package/lib/get-msg.js +46 -0
- package/lib/get-msg.js.map +1 -0
- package/lib/icqq-inbound.d.ts +114 -0
- package/lib/icqq-inbound.d.ts.map +1 -0
- package/lib/icqq-inbound.js +495 -0
- package/lib/icqq-inbound.js.map +1 -0
- package/lib/icqq-side-events.d.ts +34 -0
- package/lib/icqq-side-events.d.ts.map +1 -0
- package/lib/icqq-side-events.js +194 -0
- package/lib/icqq-side-events.js.map +1 -0
- package/lib/index.d.ts +4 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/ipc-client.d.ts +7 -2
- package/lib/ipc-client.d.ts.map +1 -1
- package/lib/ipc-client.js +74 -16
- package/lib/ipc-client.js.map +1 -1
- package/lib/protocol.d.ts +3 -10
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +2 -0
- package/lib/protocol.js.map +1 -1
- package/lib/routes.d.ts +1 -1
- package/lib/routes.d.ts.map +1 -1
- package/lib/types.d.ts +44 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/typing-indicator-example.d.ts +108 -0
- package/lib/typing-indicator-example.d.ts.map +1 -0
- package/lib/typing-indicator-example.js +220 -0
- package/lib/typing-indicator-example.js.map +1 -0
- package/lib/typing-indicator.d.ts +87 -0
- package/lib/typing-indicator.d.ts.map +1 -0
- package/lib/typing-indicator.js +225 -0
- package/lib/typing-indicator.js.map +1 -0
- package/package.json +18 -12
- package/src/bot.ts +524 -149
- package/src/cq-message.ts +120 -0
- package/src/forward-msg.ts +433 -0
- package/src/get-msg.ts +56 -0
- package/src/icqq-inbound.ts +616 -0
- package/src/icqq-side-events.ts +228 -0
- package/src/index.ts +10 -2
- package/src/ipc-client.ts +76 -16
- package/src/protocol.ts +4 -10
- package/src/routes.ts +1 -1
- package/src/types.ts +45 -0
- package/src/typing-indicator-example.ts +269 -0
- package/src/typing-indicator.ts +312 -0
package/lib/bot.js
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { formatCompact, Message, segment } from 'zhin.js';
|
|
8
8
|
import { IpcClient } from "./ipc-client.js";
|
|
9
|
-
import { Actions
|
|
9
|
+
import { Actions } from "./protocol.js";
|
|
10
|
+
import { findIcqqNestedMessageSource, InboundMessageDeduper, isIcqqMessagePostType, normalizeIcqqInboundMessage, quotedPayloadFromIcqqSource, resolveIcqqQuoteIdFromEvent, resolveQuoteIdFromIcqqSource, shouldSkipSelfInboundMessage, unwrapIcqqIpcEventPayload, } from "./icqq-inbound.js";
|
|
11
|
+
import { buildIcqqIpcMessage as buildIcqqIpcMessageImpl, parseCqMessage as parseCqMessageImpl, toCqString as toCqStringImpl, } from "./cq-message.js";
|
|
12
|
+
import { formatIcqqMetaLog, formatIcqqNotice, formatIcqqRequest, isIcqqMetaPayload, isIcqqNoticePayload, isIcqqRequestPayload, resolveIcqqEventPostType, resolveSideEventDedupeKey, shouldRefreshListsOnMeta, } from "./icqq-side-events.js";
|
|
13
|
+
import { enableTypingIndicator } from "./typing-indicator.js";
|
|
14
|
+
import { parseIcqqGetMsgResponse } from "./get-msg.js";
|
|
15
|
+
import { enrichQuotedPayloadWithForward, isForwardPlaceholderPayload } from "./forward-msg.js";
|
|
10
16
|
export class IcqqBot {
|
|
11
17
|
adapter;
|
|
12
18
|
$connected = false;
|
|
@@ -17,10 +23,16 @@ export class IcqqBot {
|
|
|
17
23
|
/** 缓存的群列表 */
|
|
18
24
|
groups = new Map();
|
|
19
25
|
subscriptions = [];
|
|
26
|
+
/** 事件去重:覆盖多端回流/服务端重复推送等场景 */
|
|
27
|
+
inboundDeduper = new InboundMessageDeduper();
|
|
28
|
+
/** MessageEvent.source 解析结果,供 $getMsg 优先命中 */
|
|
29
|
+
quotedSourceCache = new Map();
|
|
20
30
|
/** 用户主动断开时为 true,阻止自动重连 */
|
|
21
31
|
intentionalDisconnect = false;
|
|
22
32
|
/** 是否已有重连循环在跑(避免多次 schedule 叠套) */
|
|
23
33
|
reconnectRunning = false;
|
|
34
|
+
/** Typing Indicator 管理器 */
|
|
35
|
+
$typingIndicator;
|
|
24
36
|
get $id() {
|
|
25
37
|
return this.$config.name;
|
|
26
38
|
}
|
|
@@ -42,12 +54,46 @@ export class IcqqBot {
|
|
|
42
54
|
port: rpc?.port,
|
|
43
55
|
}));
|
|
44
56
|
await this.rebindIpcSession();
|
|
57
|
+
// 根据配置自动启用 Typing Indicator
|
|
58
|
+
this.initTypingIndicator();
|
|
45
59
|
this.logger.info(formatCompact({
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
机器人: this.$id,
|
|
61
|
+
好友数: this.friends.size,
|
|
62
|
+
群组数: this.groups.size,
|
|
63
|
+
思考提示: this.$typingIndicator ? '已启用' : '未启用',
|
|
49
64
|
}));
|
|
50
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* 初始化 Typing Indicator
|
|
68
|
+
* 根据配置自动启用或禁用
|
|
69
|
+
*/
|
|
70
|
+
initTypingIndicator() {
|
|
71
|
+
const typingConfig = this.$config.typingIndicator;
|
|
72
|
+
// 如果配置中明确设置为 false,则不启用
|
|
73
|
+
if (typingConfig?.enabled === false) {
|
|
74
|
+
this.logger.debug(formatCompact({
|
|
75
|
+
bot: this.$id,
|
|
76
|
+
typingIndicator: 'disabled_by_config',
|
|
77
|
+
}));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// 启用 Typing Indicator
|
|
81
|
+
try {
|
|
82
|
+
this.$typingIndicator = enableTypingIndicator(this, typingConfig);
|
|
83
|
+
this.logger.debug(formatCompact({
|
|
84
|
+
bot: this.$id,
|
|
85
|
+
typingIndicator: 'enabled',
|
|
86
|
+
emoji: typingConfig?.defaultEmoji || '⏳',
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
this.logger.warn(formatCompact({
|
|
91
|
+
bot: this.$id,
|
|
92
|
+
typingIndicator: 'failed',
|
|
93
|
+
error: error instanceof Error ? error.message : String(error),
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
51
97
|
/** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
|
|
52
98
|
async rebindIpcSession() {
|
|
53
99
|
const uin = Number(this.$config.name);
|
|
@@ -73,14 +119,9 @@ export class IcqqBot {
|
|
|
73
119
|
this.ipc.setOnRemoteDisconnect(null);
|
|
74
120
|
}
|
|
75
121
|
await this.refreshLists();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
for (const [gid] of this.groups) {
|
|
81
|
-
const sub = this.ipc.subscribe(Actions.SUBSCRIBE, { type: "group", id: gid }, (event) => this.handleEvent(event));
|
|
82
|
-
this.subscriptions.push(sub);
|
|
83
|
-
}
|
|
122
|
+
// 新版 icqq cli 在认证后自动广播事件,订阅过滤改为客户端侧完成。
|
|
123
|
+
const sub = this.ipc.subscribe(Actions.SUBSCRIBE, {}, (event) => this.handleEvent(event));
|
|
124
|
+
this.subscriptions.push(sub);
|
|
84
125
|
this.$connected = true;
|
|
85
126
|
}
|
|
86
127
|
/** IPC/RPC 意外断开时调度重连(指数退避) */
|
|
@@ -157,52 +198,196 @@ export class IcqqBot {
|
|
|
157
198
|
// ── 断开 ───────────────────────────────────────────────────────────
|
|
158
199
|
async $disconnect() {
|
|
159
200
|
this.intentionalDisconnect = true;
|
|
201
|
+
// 停止所有 Typing Indicator
|
|
202
|
+
if (this.$typingIndicator) {
|
|
203
|
+
await this.$typingIndicator.stopAll().catch(() => { });
|
|
204
|
+
this.$typingIndicator = undefined;
|
|
205
|
+
}
|
|
160
206
|
this.ipc?.setOnRemoteDisconnect(null);
|
|
161
207
|
for (const sub of this.subscriptions) {
|
|
162
208
|
await sub.unsubscribe().catch(() => { });
|
|
163
209
|
}
|
|
164
210
|
this.subscriptions = [];
|
|
211
|
+
this.inboundDeduper.clear();
|
|
165
212
|
this.ipc?.close();
|
|
166
213
|
this.$connected = false;
|
|
167
214
|
this.logger.info(formatCompact({ op: "disconnect", bot: this.$id }));
|
|
168
215
|
}
|
|
169
216
|
// ── 消息处理 ───────────────────────────────────────────────────────
|
|
170
217
|
handleEvent(event) {
|
|
171
|
-
const
|
|
172
|
-
if (!
|
|
218
|
+
const payload = unwrapIcqqIpcEventPayload(event);
|
|
219
|
+
if (!payload || typeof payload !== "object") {
|
|
220
|
+
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
221
|
+
this.logger.info(formatCompact({
|
|
222
|
+
ipc_skip: "no_payload",
|
|
223
|
+
ipc_event: event.event,
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
229
|
+
this.logger.info(formatCompact({
|
|
230
|
+
ipc_raw: JSON.stringify(payload).slice(0, 800),
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
if (isIcqqNoticePayload(payload)) {
|
|
234
|
+
this.handleNoticeEvent(payload);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (isIcqqRequestPayload(payload)) {
|
|
238
|
+
this.handleRequestEvent(payload);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (isIcqqMetaPayload(payload)) {
|
|
242
|
+
this.handleMetaEvent(payload);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!isIcqqMessagePostType(payload)) {
|
|
246
|
+
const postType = resolveIcqqEventPostType(payload);
|
|
247
|
+
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
248
|
+
this.logger.info(formatCompact({ ipc_skip: postType ?? "unknown_event" }));
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const data = payload;
|
|
253
|
+
if (shouldSkipSelfInboundMessage(data)) {
|
|
254
|
+
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
255
|
+
this.logger.info(formatCompact({ ipc_skip: "self_message" }));
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const normalized = normalizeIcqqInboundMessage(data);
|
|
260
|
+
if (!normalized) {
|
|
261
|
+
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
262
|
+
this.logger.info(formatCompact({ ipc_skip: "normalize_failed" }));
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!this.inboundDeduper.shouldProcess(normalized.messageId)) {
|
|
267
|
+
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
268
|
+
this.logger.info(formatCompact({ ipc_dedupe: normalized.messageId }));
|
|
269
|
+
}
|
|
173
270
|
return;
|
|
174
|
-
|
|
271
|
+
}
|
|
272
|
+
void this.dispatchInboundMessage(data, normalized);
|
|
273
|
+
}
|
|
274
|
+
async dispatchInboundMessage(data, normalized) {
|
|
275
|
+
this.logIpcInboundPayload(data, normalized);
|
|
276
|
+
await this.primeQuotedSourceCache(data.source ?? findIcqqNestedMessageSource(data));
|
|
277
|
+
const message = this.$formatMessage(normalized);
|
|
175
278
|
this.adapter.emit("message.receive", message);
|
|
176
|
-
this.logger.debug(`${this.$id} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`);
|
|
279
|
+
this.logger.debug(`${this.$id} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}${message.$quote_id ? ` quote_id=${message.$quote_id}` : ""}`);
|
|
280
|
+
}
|
|
281
|
+
handleNoticeEvent(event) {
|
|
282
|
+
const dedupeKey = resolveSideEventDedupeKey(event, "notice");
|
|
283
|
+
if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
|
|
284
|
+
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
285
|
+
this.logger.info(formatCompact({ ipc_dedupe: dedupeKey }));
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const notice = formatIcqqNotice(event, this.$config.name);
|
|
290
|
+
this.adapter.emit("notice.receive", notice);
|
|
291
|
+
this.logger.info(formatCompact({
|
|
292
|
+
notice: notice.$type,
|
|
293
|
+
channel: `${notice.$channel.type}(${notice.$channel.id})`,
|
|
294
|
+
bot: this.$id,
|
|
295
|
+
sub_type: notice.$subType,
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
handleRequestEvent(event) {
|
|
299
|
+
const dedupeKey = resolveSideEventDedupeKey(event, "request");
|
|
300
|
+
if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
|
|
301
|
+
if (process.env.ICQQ_IPC_LOG_RAW === "1") {
|
|
302
|
+
this.logger.info(formatCompact({ ipc_dedupe: dedupeKey }));
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const request = formatIcqqRequest(event, this.$config.name, this.ipc);
|
|
307
|
+
this.adapter.emit("request.receive", request);
|
|
308
|
+
this.logger.info(formatCompact({
|
|
309
|
+
request: request.$type,
|
|
310
|
+
channel: `${request.$channel.type}(${request.$channel.id})`,
|
|
311
|
+
bot: this.$id,
|
|
312
|
+
from: request.$sender.id,
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
handleMetaEvent(event) {
|
|
316
|
+
const dedupeKey = resolveSideEventDedupeKey(event, "meta");
|
|
317
|
+
if (!this.inboundDeduper.shouldProcess(dedupeKey))
|
|
318
|
+
return;
|
|
319
|
+
this.logger.debug(formatIcqqMetaLog(event));
|
|
320
|
+
if (shouldRefreshListsOnMeta(event)) {
|
|
321
|
+
void this.refreshLists().catch((e) => {
|
|
322
|
+
this.logger.warn(formatCompact({
|
|
323
|
+
op: "refresh_lists",
|
|
324
|
+
ok: false,
|
|
325
|
+
error: e instanceof Error ? e.message : String(e),
|
|
326
|
+
}));
|
|
327
|
+
});
|
|
328
|
+
}
|
|
177
329
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
330
|
+
/** 调试 IPC 入站字段:默认 debug;设 ICQQ_IPC_LOG_RAW=1 则 info 打完整 payload */
|
|
331
|
+
logIpcInboundPayload(data, normalized) {
|
|
332
|
+
const ext = data;
|
|
333
|
+
const keys = Object.keys(ext);
|
|
334
|
+
const idHints = {};
|
|
335
|
+
for (const key of keys) {
|
|
336
|
+
if (/message|msg|seq|id|source/i.test(key)) {
|
|
337
|
+
idHints[key] = ext[key];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const preview = JSON.stringify(data);
|
|
341
|
+
const compact = formatCompact({
|
|
342
|
+
post_type: data.post_type ?? "legacy",
|
|
343
|
+
message_type: data.message_type ?? data.type,
|
|
344
|
+
ipc_message_id: normalized.messageId,
|
|
345
|
+
id_source: normalized.idSource,
|
|
346
|
+
ipc_keys: keys.join(","),
|
|
347
|
+
...(Object.keys(idHints).length ? { ipc_id_hints: JSON.stringify(idHints) } : {}),
|
|
348
|
+
});
|
|
349
|
+
this.logger.debug(compact);
|
|
350
|
+
this.logger.debug(formatCompact({ ipc_payload: preview.slice(0, 1200) }));
|
|
351
|
+
}
|
|
352
|
+
$formatMessage(input) {
|
|
353
|
+
const normalized = "messageId" in input
|
|
354
|
+
? input
|
|
355
|
+
: normalizeIcqqInboundMessage(input);
|
|
356
|
+
if (!normalized) {
|
|
357
|
+
throw new Error("无法解析 icqq 入站消息");
|
|
358
|
+
}
|
|
359
|
+
const raw = normalized.raw;
|
|
185
360
|
const senderInfo = {
|
|
186
|
-
id:
|
|
187
|
-
name:
|
|
361
|
+
id: normalized.userId,
|
|
362
|
+
name: normalized.nickname,
|
|
188
363
|
};
|
|
364
|
+
const quoteId = Message.quoteIdFromContent(normalized.content) ??
|
|
365
|
+
resolveIcqqQuoteIdFromEvent(normalized.raw);
|
|
366
|
+
Message.alignReplySegments(normalized.content, quoteId);
|
|
189
367
|
const result = Message.from(raw, {
|
|
190
|
-
$id:
|
|
368
|
+
$id: normalized.messageId,
|
|
191
369
|
$adapter: "icqq",
|
|
192
370
|
$bot: this.$config.name,
|
|
193
371
|
$sender: senderInfo,
|
|
194
|
-
$channel: {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
372
|
+
$channel: {
|
|
373
|
+
id: normalized.channelId,
|
|
374
|
+
type: normalized.channelType,
|
|
375
|
+
},
|
|
376
|
+
$content: normalized.content,
|
|
377
|
+
$quote_id: quoteId,
|
|
378
|
+
$raw: normalized.rawMessage,
|
|
379
|
+
$timestamp: normalized.timestampMs,
|
|
198
380
|
$recall: async () => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
381
|
+
if (normalized.idSource === "synthetic") {
|
|
382
|
+
this.logger.warn(formatCompact({
|
|
383
|
+
op: "recall",
|
|
384
|
+
bot: this.$id,
|
|
385
|
+
ok: false,
|
|
386
|
+
error: "no message_id in push",
|
|
387
|
+
}));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
await this.$recallMessage(result.$id);
|
|
206
391
|
},
|
|
207
392
|
$reply: async (content, quote) => {
|
|
208
393
|
if (!Array.isArray(content))
|
|
@@ -223,6 +408,65 @@ export class IcqqBot {
|
|
|
223
408
|
});
|
|
224
409
|
return result;
|
|
225
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* 有 source.message_id 时用 get_msg 拉全量正文;否则仅用 source 内联摘要。
|
|
413
|
+
*/
|
|
414
|
+
async primeQuotedSourceCache(source) {
|
|
415
|
+
if (!source)
|
|
416
|
+
return;
|
|
417
|
+
const quoteId = resolveQuoteIdFromIcqqSource(source);
|
|
418
|
+
const s = source;
|
|
419
|
+
const hasCanonicalId = quoteId &&
|
|
420
|
+
s.message_id != null &&
|
|
421
|
+
String(s.message_id).trim() === quoteId;
|
|
422
|
+
if (hasCanonicalId) {
|
|
423
|
+
try {
|
|
424
|
+
await this.fetchQuotedMessagePayload(quoteId);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
catch (e) {
|
|
428
|
+
this.logger.debug(formatCompact({
|
|
429
|
+
op: "quote_get_msg",
|
|
430
|
+
message_id: quoteId,
|
|
431
|
+
ok: false,
|
|
432
|
+
error: e instanceof Error ? e.message : String(e),
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const sourcePayload = quotedPayloadFromIcqqSource(source);
|
|
437
|
+
if (!sourcePayload)
|
|
438
|
+
return;
|
|
439
|
+
const enriched = await enrichQuotedPayloadWithForward(this.ipc, sourcePayload);
|
|
440
|
+
this.quotedSourceCache.set(enriched.messageId, enriched);
|
|
441
|
+
}
|
|
442
|
+
async fetchQuotedMessagePayload(messageId) {
|
|
443
|
+
const resp = await this.ipc.request(Actions.GET_MSG, {
|
|
444
|
+
message_id: messageId,
|
|
445
|
+
});
|
|
446
|
+
if (!resp.ok) {
|
|
447
|
+
throw new Error(resp.error ?? "get_msg failed");
|
|
448
|
+
}
|
|
449
|
+
const payload = parseIcqqGetMsgResponse(messageId, resp.data);
|
|
450
|
+
const enriched = await enrichQuotedPayloadWithForward(this.ipc, payload, resp.data);
|
|
451
|
+
if (isForwardPlaceholderPayload(enriched) &&
|
|
452
|
+
!String(Array.isArray(enriched.content)
|
|
453
|
+
? segment.raw(enriched.content)
|
|
454
|
+
: enriched.content ?? "").includes("[Merged chat history")) {
|
|
455
|
+
this.logger.debug(formatCompact({
|
|
456
|
+
op: "forward_unresolved",
|
|
457
|
+
message_id: messageId,
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
this.quotedSourceCache.set(messageId, enriched);
|
|
461
|
+
return enriched;
|
|
462
|
+
}
|
|
463
|
+
async $getMsg(messageId) {
|
|
464
|
+
const cached = this.quotedSourceCache.get(messageId);
|
|
465
|
+
if (cached) {
|
|
466
|
+
return enrichQuotedPayloadWithForward(this.ipc, cached);
|
|
467
|
+
}
|
|
468
|
+
return this.fetchQuotedMessagePayload(messageId);
|
|
469
|
+
}
|
|
226
470
|
// ── 撤回 ───────────────────────────────────────────────────────────
|
|
227
471
|
async $recallMessage(id) {
|
|
228
472
|
const resp = await this.ipc.request(Actions.RECALL_MSG, {
|
|
@@ -239,134 +483,170 @@ export class IcqqBot {
|
|
|
239
483
|
}
|
|
240
484
|
// ── 发送消息 ───────────────────────────────────────────────────────
|
|
241
485
|
async $sendMessage(options) {
|
|
242
|
-
const
|
|
486
|
+
const message = buildIcqqIpcMessageImpl(options.content);
|
|
243
487
|
let action;
|
|
244
488
|
let params;
|
|
245
489
|
switch (options.type) {
|
|
246
490
|
case "private":
|
|
247
491
|
action = Actions.SEND_PRIVATE_MSG;
|
|
248
|
-
params = { user_id: Number(options.id), message
|
|
492
|
+
params = { user_id: Number(options.id), message };
|
|
249
493
|
break;
|
|
250
494
|
case "group":
|
|
251
495
|
action = Actions.SEND_GROUP_MSG;
|
|
252
|
-
params = { group_id: Number(options.id), message
|
|
496
|
+
params = { group_id: Number(options.id), message };
|
|
253
497
|
break;
|
|
254
498
|
default:
|
|
255
499
|
throw new Error(`不支持的频道类型: ${options.type}`);
|
|
256
500
|
}
|
|
257
501
|
const resp = await this.ipc.request(action, params);
|
|
258
502
|
if (!resp.ok) {
|
|
503
|
+
this.logger.debug(formatCompact({
|
|
504
|
+
op: action,
|
|
505
|
+
...params,
|
|
506
|
+
}));
|
|
259
507
|
throw new Error(`发送消息失败: ${resp.error}`);
|
|
260
508
|
}
|
|
261
509
|
const messageId = String(resp.data?.message_id ?? `sent_${Date.now()}`);
|
|
262
510
|
this.logger.debug(`${this.$id} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
|
|
263
511
|
return messageId;
|
|
264
512
|
}
|
|
265
|
-
|
|
266
|
-
// ── CQ 码解析工具 ──────────────────────────────────────────────────
|
|
267
|
-
(function (IcqqBot) {
|
|
513
|
+
// ── 消息回应 ───────────────────────────────────────────────────────
|
|
268
514
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
515
|
+
* 表情符号到 reaction id 的映射
|
|
516
|
+
* QQ 使用数字 ID 来标识表情,而不是 Unicode 字符
|
|
271
517
|
*/
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
518
|
+
static EMOJI_MAP = {
|
|
519
|
+
'⏳': '1468368274', // 沙漏
|
|
520
|
+
'👍': '128077', // 竖起大拇指
|
|
521
|
+
'❤️': '10084', // 红心
|
|
522
|
+
'😊': '128522', // 微笑
|
|
523
|
+
'🎉': '127881', // 派对
|
|
524
|
+
'🔥': '128293', // 火
|
|
525
|
+
'✅': '9989', // 勾选
|
|
526
|
+
'❌': '10060', // 叉号
|
|
527
|
+
'⭐': '11088', // 星星
|
|
528
|
+
'💯': '128175', // 一百分
|
|
529
|
+
};
|
|
530
|
+
/**
|
|
531
|
+
* 将表情符号转换为 reaction id
|
|
532
|
+
*/
|
|
533
|
+
getEmojiId(emoji) {
|
|
534
|
+
// 如果已经是数字 ID,直接返回
|
|
535
|
+
if (/^\d+$/.test(emoji)) {
|
|
536
|
+
return emoji;
|
|
537
|
+
}
|
|
538
|
+
// 从映射中查找
|
|
539
|
+
const id = IcqqBot.EMOJI_MAP[emoji];
|
|
540
|
+
if (id) {
|
|
541
|
+
return id;
|
|
542
|
+
}
|
|
543
|
+
// 默认返回 Unicode 码点
|
|
544
|
+
return String(emoji.codePointAt(0) || 0);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* 添加消息回应(表情)
|
|
548
|
+
*
|
|
549
|
+
* @param messageId - 消息 ID
|
|
550
|
+
* @param emoji - 表情符号或 reaction id
|
|
551
|
+
* @returns 反应 ID,可用于后续移除
|
|
552
|
+
*/
|
|
553
|
+
async $addReaction(messageId, emoji) {
|
|
554
|
+
try {
|
|
555
|
+
const emojiId = this.getEmojiId(emoji);
|
|
556
|
+
const resp = await this.ipc.request(Actions.GROUP_SET_REACTION, {
|
|
557
|
+
message_id: messageId,
|
|
558
|
+
id: emojiId,
|
|
559
|
+
});
|
|
560
|
+
if (!resp.ok) {
|
|
561
|
+
this.logger.warn(formatCompact({
|
|
562
|
+
op: "add_reaction",
|
|
563
|
+
bot: this.$id,
|
|
564
|
+
message_id: messageId,
|
|
565
|
+
emoji,
|
|
566
|
+
id: emojiId,
|
|
567
|
+
ok: false,
|
|
568
|
+
error: resp.error,
|
|
569
|
+
}));
|
|
570
|
+
return null;
|
|
320
571
|
}
|
|
321
|
-
|
|
572
|
+
this.logger.debug(formatCompact({
|
|
573
|
+
op: "add_reaction",
|
|
574
|
+
bot: this.$id,
|
|
575
|
+
message_id: messageId,
|
|
576
|
+
emoji,
|
|
577
|
+
id: emojiId,
|
|
578
|
+
ok: true,
|
|
579
|
+
}));
|
|
580
|
+
// 返回 reaction id,供 $removeReaction 直接使用
|
|
581
|
+
return emojiId;
|
|
322
582
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
583
|
+
catch (error) {
|
|
584
|
+
this.logger.warn(formatCompact({
|
|
585
|
+
op: "add_reaction",
|
|
586
|
+
bot: this.$id,
|
|
587
|
+
message_id: messageId,
|
|
588
|
+
emoji,
|
|
589
|
+
ok: false,
|
|
590
|
+
error: error instanceof Error ? error.message : String(error),
|
|
591
|
+
}));
|
|
592
|
+
return null;
|
|
328
593
|
}
|
|
329
|
-
return segments.length ? segments : [{ type: "text", data: { text: raw } }];
|
|
330
594
|
}
|
|
331
|
-
IcqqBot.parseCqMessage = parseCqMessage;
|
|
332
595
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
596
|
+
* 移除消息回应
|
|
597
|
+
*
|
|
598
|
+
* @param messageId - 消息 ID
|
|
599
|
+
* @param reactionId - 反应 ID(由 $addReaction 返回)
|
|
335
600
|
*/
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
601
|
+
async $removeReaction(messageId, reactionId) {
|
|
602
|
+
try {
|
|
603
|
+
// 兼容两种格式:
|
|
604
|
+
// 1) 旧格式 reaction_{id}_{timestamp}
|
|
605
|
+
// 2) 新格式直接为 id
|
|
606
|
+
const parts = reactionId.split('_');
|
|
607
|
+
const emojiId = parts.length >= 2 ? parts[1] : reactionId;
|
|
608
|
+
const resp = await this.ipc.request(Actions.GROUP_DEL_REACTION, {
|
|
609
|
+
message_id: messageId,
|
|
610
|
+
id: emojiId,
|
|
611
|
+
});
|
|
612
|
+
if (!resp.ok) {
|
|
613
|
+
this.logger.warn(formatCompact({
|
|
614
|
+
op: "remove_reaction",
|
|
615
|
+
bot: this.$id,
|
|
616
|
+
message_id: messageId,
|
|
617
|
+
reaction_id: reactionId,
|
|
618
|
+
id: emojiId,
|
|
619
|
+
ok: false,
|
|
620
|
+
error: resp.error,
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
this.logger.debug(formatCompact({
|
|
625
|
+
op: "remove_reaction",
|
|
626
|
+
bot: this.$id,
|
|
627
|
+
message_id: messageId,
|
|
628
|
+
reaction_id: reactionId,
|
|
629
|
+
id: emojiId,
|
|
630
|
+
ok: true,
|
|
631
|
+
}));
|
|
366
632
|
}
|
|
367
|
-
}
|
|
368
|
-
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
this.logger.warn(formatCompact({
|
|
636
|
+
op: "remove_reaction",
|
|
637
|
+
bot: this.$id,
|
|
638
|
+
message_id: messageId,
|
|
639
|
+
reaction_id: reactionId,
|
|
640
|
+
ok: false,
|
|
641
|
+
error: error instanceof Error ? error.message : String(error),
|
|
642
|
+
}));
|
|
643
|
+
}
|
|
369
644
|
}
|
|
370
|
-
|
|
645
|
+
}
|
|
646
|
+
/** @deprecated 使用 `./cq-message.js` 导出 */
|
|
647
|
+
(function (IcqqBot) {
|
|
648
|
+
IcqqBot.parseCqMessage = parseCqMessageImpl;
|
|
649
|
+
IcqqBot.buildIcqqIpcMessage = buildIcqqIpcMessageImpl;
|
|
650
|
+
IcqqBot.toCqString = toCqStringImpl;
|
|
371
651
|
})(IcqqBot || (IcqqBot = {}));
|
|
372
652
|
//# sourceMappingURL=bot.js.map
|