@zhin.js/adapter-icqq 6.0.2 → 6.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +32 -437
  3. package/adapters/icqq.ts +30 -0
  4. package/agent/tools/announce.ts +2 -2
  5. package/agent/tools/essence.ts +2 -2
  6. package/agent/tools/friend_list.ts +2 -2
  7. package/agent/tools/group_files.ts +2 -2
  8. package/agent/tools/group_invite.ts +2 -2
  9. package/agent/tools/group_list.ts +2 -2
  10. package/agent/tools/group_sign.ts +2 -2
  11. package/agent/tools/list_muted.ts +2 -2
  12. package/agent/tools/poke.ts +2 -2
  13. package/agent/tools/send_user_like.ts +2 -2
  14. package/agent/tools/set_anonymous.ts +2 -2
  15. package/agent/tools/set_title.ts +2 -2
  16. package/lib/agent-prompt.d.ts +2 -0
  17. package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
  18. package/lib/cq-message.d.ts +9 -0
  19. package/lib/{src/cq-message.js → cq-message.js} +0 -1
  20. package/lib/endpoint.d.ts +51 -0
  21. package/lib/endpoint.js +483 -0
  22. package/lib/forward-msg.d.ts +26 -0
  23. package/lib/{src/forward-msg.js → forward-msg.js} +0 -1
  24. package/lib/icqq-agent-deps.d.ts +25 -0
  25. package/lib/icqq-agent-deps.js +32 -0
  26. package/lib/icqq-guild.d.ts +47 -0
  27. package/lib/{src/icqq-guild.js → icqq-guild.js} +4 -2
  28. package/lib/icqq-inbound.d.ts +139 -0
  29. package/lib/{src/icqq-inbound.js → icqq-inbound.js} +27 -3
  30. package/lib/index.d.ts +6 -0
  31. package/lib/index.js +6 -0
  32. package/lib/ipc-client.d.ts +73 -0
  33. package/lib/{src/ipc-client.js → ipc-client.js} +0 -1
  34. package/lib/outbound-media.d.ts +13 -0
  35. package/lib/{src/outbound-media.js → outbound-media.js} +0 -1
  36. package/lib/protocol.d.ts +269 -0
  37. package/lib/{src/protocol.js → protocol.js} +124 -2
  38. package/lib/types.d.ts +72 -0
  39. package/lib/{src/types.js → types.js} +0 -1
  40. package/package.json +49 -45
  41. package/plugin.ts +32 -0
  42. package/schema.json +35 -0
  43. package/src/endpoint.ts +407 -796
  44. package/src/icqq-agent-deps.ts +48 -9
  45. package/src/icqq-guild.ts +1 -2
  46. package/src/icqq-inbound.ts +31 -2
  47. package/src/index.ts +35 -69
  48. package/src/protocol.ts +191 -1
  49. package/client/ICQQManagement.tsx +0 -147
  50. package/client/LoginAssistPanel.tsx +0 -300
  51. package/client/index.tsx +0 -11
  52. package/client/tsconfig.json +0 -7
  53. package/client/utils/api.ts +0 -30
  54. package/dist/index.js +0 -34
  55. package/lib/agent/tools/announce.js +0 -21
  56. package/lib/agent/tools/announce.js.map +0 -1
  57. package/lib/agent/tools/essence.js +0 -22
  58. package/lib/agent/tools/essence.js.map +0 -1
  59. package/lib/agent/tools/friend_list.js +0 -19
  60. package/lib/agent/tools/friend_list.js.map +0 -1
  61. package/lib/agent/tools/group_files.js +0 -29
  62. package/lib/agent/tools/group_files.js.map +0 -1
  63. package/lib/agent/tools/group_invite.js +0 -20
  64. package/lib/agent/tools/group_invite.js.map +0 -1
  65. package/lib/agent/tools/group_list.js +0 -20
  66. package/lib/agent/tools/group_list.js.map +0 -1
  67. package/lib/agent/tools/group_sign.js +0 -19
  68. package/lib/agent/tools/group_sign.js.map +0 -1
  69. package/lib/agent/tools/list_muted.js +0 -23
  70. package/lib/agent/tools/list_muted.js.map +0 -1
  71. package/lib/agent/tools/poke.js +0 -20
  72. package/lib/agent/tools/poke.js.map +0 -1
  73. package/lib/agent/tools/send_user_like.js +0 -22
  74. package/lib/agent/tools/send_user_like.js.map +0 -1
  75. package/lib/agent/tools/set_anonymous.js +0 -22
  76. package/lib/agent/tools/set_anonymous.js.map +0 -1
  77. package/lib/agent/tools/set_title.js +0 -25
  78. package/lib/agent/tools/set_title.js.map +0 -1
  79. package/lib/src/adapter.js +0 -128
  80. package/lib/src/adapter.js.map +0 -1
  81. package/lib/src/agent-prompt.js.map +0 -1
  82. package/lib/src/cq-message.js.map +0 -1
  83. package/lib/src/endpoint.js +0 -755
  84. package/lib/src/endpoint.js.map +0 -1
  85. package/lib/src/forward-msg.js.map +0 -1
  86. package/lib/src/get-msg.js +0 -46
  87. package/lib/src/get-msg.js.map +0 -1
  88. package/lib/src/icqq-agent-deps.js +0 -10
  89. package/lib/src/icqq-agent-deps.js.map +0 -1
  90. package/lib/src/icqq-guild.js.map +0 -1
  91. package/lib/src/icqq-inbound.js.map +0 -1
  92. package/lib/src/icqq-login-assist-bridge.js +0 -148
  93. package/lib/src/icqq-login-assist-bridge.js.map +0 -1
  94. package/lib/src/icqq-side-events.js +0 -130
  95. package/lib/src/icqq-side-events.js.map +0 -1
  96. package/lib/src/index.js +0 -50
  97. package/lib/src/index.js.map +0 -1
  98. package/lib/src/ipc-client.js.map +0 -1
  99. package/lib/src/login-assist-routes.js +0 -29
  100. package/lib/src/login-assist-routes.js.map +0 -1
  101. package/lib/src/login-ipc-contract.js +0 -35
  102. package/lib/src/login-ipc-contract.js.map +0 -1
  103. package/lib/src/outbound-media.js.map +0 -1
  104. package/lib/src/protocol.js.map +0 -1
  105. package/lib/src/routes.js +0 -67
  106. package/lib/src/routes.js.map +0 -1
  107. package/lib/src/segment-mapper.js +0 -2
  108. package/lib/src/segment-mapper.js.map +0 -1
  109. package/lib/src/tools/index.js +0 -23
  110. package/lib/src/tools/index.js.map +0 -1
  111. package/lib/src/types.js.map +0 -1
  112. package/plugin.yml +0 -3
  113. package/src/adapter.ts +0 -151
  114. package/src/get-msg.ts +0 -56
  115. package/src/icqq-login-assist-bridge.ts +0 -196
  116. package/src/icqq-side-events.ts +0 -143
  117. package/src/login-assist-routes.ts +0 -39
  118. package/src/login-ipc-contract.ts +0 -52
  119. package/src/routes.ts +0 -83
  120. package/src/segment-mapper.ts +0 -1
  121. package/src/tools/index.ts +0 -31
@@ -1,755 +0,0 @@
1
- /**
2
- * ICQQ Bot:通过 @icqqjs/cli 守护进程 IPC 通信,实现 zhin Endpoint 接口
3
- *
4
- * 不再直接依赖 @icqqjs/icqq 协议库。
5
- * 登录由 `icqq login` 完成,本 Endpoint 只负责连接守护进程并收发消息。
6
- */
7
- import { formatCompact, Message, segment, expandInteractiveSegmentsInContent, resolveSideEventDedupeKey, } from 'zhin.js';
8
- import { IpcClient } from "./ipc-client.js";
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 { materializeOutboundBase64, resolveIcqqOutboundMediaMode, } from "./outbound-media.js";
13
- import { formatIcqqMetaLog, formatIcqqNotice, formatIcqqRequest, isIcqqMetaPayload, isIcqqNoticePayload, isIcqqRequestPayload, resolveIcqqEventPostType, shouldRefreshListsOnMeta, } from "./icqq-side-events.js";
14
- import { disposeIcqqLoginAssistBridge, handleIcqqLoginIpcEvent, } from "./icqq-login-assist-bridge.js";
15
- import { parseIcqqGetMsgResponse } from "./get-msg.js";
16
- import { enrichQuotedPayloadWithForward, isForwardPlaceholderPayload } from "./forward-msg.js";
17
- import { IcqqGuildCatalog, isIcqqGuildIpcEvent, normalizeIcqqGuildInboundMessage, } from "./icqq-guild.js";
18
- export class IcqqEndpoint {
19
- adapter;
20
- $connected = false;
21
- $config;
22
- ipc;
23
- /** 缓存的好友列表 */
24
- friends = new Map();
25
- /** 缓存的群列表 */
26
- groups = new Map();
27
- /** QQ 频道(guild)子频道缓存 */
28
- guildCatalog = new IcqqGuildCatalog();
29
- subscriptions = [];
30
- /** 事件去重:覆盖多端回流/服务端重复推送等场景 */
31
- inboundDeduper = new InboundMessageDeduper();
32
- /** MessageEvent.source 解析结果,供 $getMsg 优先命中 */
33
- quotedSourceCache = new Map();
34
- /** 用户主动断开时为 true,阻止自动重连 */
35
- intentionalDisconnect = false;
36
- /** 是否已有重连循环在跑(避免多次 schedule 叠套) */
37
- reconnectRunning = false;
38
- get $id() {
39
- return this.$config.name;
40
- }
41
- get logger() {
42
- return this.adapter.plugin.logger;
43
- }
44
- constructor(adapter, config) {
45
- this.adapter = adapter;
46
- this.$config = config;
47
- }
48
- // ── 连接 ───────────────────────────────────────────────────────────
49
- async $connect() {
50
- this.intentionalDisconnect = false;
51
- const rpc = this.$config.rpc;
52
- this.logger.debug(formatCompact({
53
- connecting: this.$id,
54
- mode: rpc ? "rpc" : "ipc",
55
- host: rpc?.host,
56
- port: rpc?.port,
57
- }));
58
- await this.rebindIpcSession();
59
- this.logger.debug(formatCompact({
60
- 机器人: this.$id,
61
- 好友数: this.friends.size,
62
- 群组数: this.groups.size,
63
- }));
64
- }
65
- /** 建立或恢复与守护进程的 IPC/RPC 会话(订阅、缓存列表) */
66
- async rebindIpcSession() {
67
- const uin = Number(this.$config.name);
68
- const rpc = this.$config.rpc;
69
- // 旧连接上的订阅句柄已失效,不再对死 socket 发 unsubscribe
70
- this.subscriptions = [];
71
- let client;
72
- if (rpc) {
73
- client = await IpcClient.connectRpc(rpc);
74
- }
75
- else {
76
- client = await IpcClient.connect(uin);
77
- }
78
- if (this.intentionalDisconnect) {
79
- client.close();
80
- throw new Error("连接已取消");
81
- }
82
- this.ipc = client;
83
- if (this.$config.autoReconnect !== false) {
84
- this.ipc.setOnRemoteDisconnect(() => this.scheduleReconnect());
85
- }
86
- else {
87
- this.ipc.setOnRemoteDisconnect(null);
88
- }
89
- await this.refreshLists();
90
- // 新版 icqq cli 在认证后自动广播事件,订阅过滤改为客户端侧完成。
91
- const sub = this.ipc.subscribe(Actions.SUBSCRIBE, {}, (event) => this.handleEvent(event));
92
- this.subscriptions.push(sub);
93
- this.$connected = true;
94
- }
95
- /** IPC/RPC 意外断开时调度重连(指数退避) */
96
- scheduleReconnect() {
97
- if (this.$config.autoReconnect === false)
98
- return;
99
- if (this.intentionalDisconnect)
100
- return;
101
- if (this.reconnectRunning)
102
- return;
103
- this.reconnectRunning = true;
104
- void this.runReconnectLoop();
105
- }
106
- async runReconnectLoop() {
107
- try {
108
- for (let attempt = 0; !this.intentionalDisconnect; attempt++) {
109
- const base = Math.min(30_000, 500 * 2 ** Math.min(attempt, 6));
110
- const jitter = Math.floor(Math.random() * 400);
111
- const delayMs = base + jitter;
112
- this.$connected = false;
113
- this.logger.warn(formatCompact({
114
- op: "disconnect",
115
- endpoint: this.$id,
116
- ok: false,
117
- delay_ms: delayMs,
118
- attempt: attempt + 1,
119
- }));
120
- await new Promise((r) => setTimeout(r, delayMs));
121
- if (this.intentionalDisconnect)
122
- break;
123
- try {
124
- await this.rebindIpcSession();
125
- this.logger.debug(formatCompact({
126
- op: "reconnect",
127
- endpoint: this.$id,
128
- friends: this.friends.size,
129
- groups: this.groups.size,
130
- }));
131
- break;
132
- }
133
- catch (e) {
134
- this.logger.warn(formatCompact({
135
- op: "reconnect",
136
- endpoint: this.$id,
137
- ok: false,
138
- error: e instanceof Error ? e.message : String(e),
139
- }));
140
- }
141
- }
142
- }
143
- finally {
144
- this.reconnectRunning = false;
145
- }
146
- }
147
- /** 刷新好友/群列表缓存 */
148
- async refreshLists() {
149
- const [flResp, glResp] = await Promise.all([
150
- this.ipc.request(Actions.LIST_FRIENDS),
151
- this.ipc.request(Actions.LIST_GROUPS),
152
- ]);
153
- this.friends.clear();
154
- if (flResp.ok && Array.isArray(flResp.data)) {
155
- for (const f of flResp.data) {
156
- this.friends.set(f.user_id, f);
157
- }
158
- }
159
- this.groups.clear();
160
- if (glResp.ok && Array.isArray(glResp.data)) {
161
- for (const g of glResp.data) {
162
- this.groups.set(g.group_id, g);
163
- }
164
- }
165
- await this.guildCatalog.syncAll(this.ipc).catch((e) => {
166
- this.logger.warn(formatCompact({
167
- op: "sync_guilds",
168
- endpoint: this.$id,
169
- ok: false,
170
- error: e instanceof Error ? e.message : String(e),
171
- }));
172
- });
173
- }
174
- /** Console:列出 guild 子频道(含 parent.guild) */
175
- getGuildChannelList() {
176
- return this.guildCatalog.getGuildChannelList();
177
- }
178
- resolveConsoleChannelNames(channelId, guildId) {
179
- return this.guildCatalog.resolveConsoleChannelNames(channelId, guildId);
180
- }
181
- // ── 断开 ───────────────────────────────────────────────────────────
182
- async $disconnect() {
183
- this.intentionalDisconnect = true;
184
- this.ipc?.setOnRemoteDisconnect(null);
185
- for (const sub of this.subscriptions) {
186
- await sub.unsubscribe().catch(() => { });
187
- }
188
- this.subscriptions = [];
189
- this.inboundDeduper.clear();
190
- disposeIcqqLoginAssistBridge(this.$id);
191
- this.ipc?.close();
192
- this.$connected = false;
193
- this.logger.debug(formatCompact({ op: "disconnect", endpoint: this.$id }));
194
- }
195
- // ── 消息处理 ───────────────────────────────────────────────────────
196
- handleEvent(event) {
197
- if (event.event?.startsWith("system.login.")) {
198
- handleIcqqLoginIpcEvent(this, event.event, event.data);
199
- return;
200
- }
201
- if (event.event === Actions.RELOAD_GUILDS) {
202
- void this.guildCatalog.syncAll(this.ipc).catch((e) => {
203
- this.logger.warn(formatCompact({
204
- op: "reload_guilds",
205
- endpoint: this.$id,
206
- ok: false,
207
- error: e instanceof Error ? e.message : String(e),
208
- }));
209
- });
210
- return;
211
- }
212
- if (isIcqqGuildIpcEvent(event.event)) {
213
- this.handleGuildMessageEvent(event);
214
- return;
215
- }
216
- const payload = unwrapIcqqIpcEventPayload(event);
217
- if (!payload || typeof payload !== "object") {
218
- if (process.env.ICQQ_IPC_LOG_RAW === "1") {
219
- this.logger.info(formatCompact({
220
- ipc_skip: "no_payload",
221
- ipc_event: event.event,
222
- }));
223
- }
224
- return;
225
- }
226
- if (process.env.ICQQ_IPC_LOG_RAW === "1") {
227
- this.logger.info(formatCompact({
228
- ipc_raw: JSON.stringify(payload).slice(0, 800),
229
- }));
230
- }
231
- if (isIcqqNoticePayload(payload)) {
232
- this.handleNoticeEvent(payload);
233
- return;
234
- }
235
- if (isIcqqRequestPayload(payload)) {
236
- this.handleRequestEvent(payload);
237
- return;
238
- }
239
- if (isIcqqMetaPayload(payload)) {
240
- this.handleMetaEvent(payload);
241
- return;
242
- }
243
- if (!isIcqqMessagePostType(payload)) {
244
- const postType = resolveIcqqEventPostType(payload);
245
- if (process.env.ICQQ_IPC_LOG_RAW === "1") {
246
- this.logger.info(formatCompact({ ipc_skip: postType ?? "unknown_event" }));
247
- }
248
- return;
249
- }
250
- const data = payload;
251
- if (shouldSkipSelfInboundMessage(data)) {
252
- if (process.env.ICQQ_IPC_LOG_RAW === "1") {
253
- this.logger.debug(formatCompact({ ipc_skip: "self_message" }));
254
- }
255
- return;
256
- }
257
- const normalized = normalizeIcqqInboundMessage(data);
258
- if (!normalized) {
259
- if (process.env.ICQQ_IPC_LOG_RAW === "1") {
260
- this.logger.debug(formatCompact({ ipc_skip: "normalize_failed" }));
261
- }
262
- return;
263
- }
264
- if (!this.inboundDeduper.shouldProcess(normalized.messageId)) {
265
- if (process.env.ICQQ_IPC_LOG_RAW === "1") {
266
- this.logger.info(formatCompact({ ipc_dedupe: normalized.messageId }));
267
- }
268
- return;
269
- }
270
- void this.dispatchInboundMessage(data, normalized);
271
- }
272
- async dispatchInboundMessage(data, normalized) {
273
- this.logIpcInboundPayload(data, normalized);
274
- await this.primeQuotedSourceCache(data.source ?? findIcqqNestedMessageSource(data));
275
- const message = this.$formatMessage(normalized);
276
- this.adapter.emit("message.receive", message);
277
- this.logger.debug(`${this.$id} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}${message.$quote_id ? ` quote_id=${message.$quote_id}` : ""}`);
278
- }
279
- handleGuildMessageEvent(event) {
280
- const payload = event.data;
281
- if (!payload || typeof payload !== "object")
282
- return;
283
- const data = payload;
284
- const normalized = normalizeIcqqGuildInboundMessage(data);
285
- if (!normalized)
286
- return;
287
- this.guildCatalog.upsertFromInbound(data);
288
- if (!this.inboundDeduper.shouldProcess(`guild:${normalized.messageId}`)) {
289
- return;
290
- }
291
- void this.dispatchGuildInboundMessage(normalized);
292
- }
293
- async dispatchGuildInboundMessage(normalized) {
294
- const message = this.$formatGuildMessage(normalized);
295
- this.adapter.emit("message.receive", message);
296
- this.logger.debug(`${this.$id} recv channel(${message.$channel.id}):${segment.raw(message.$content)}`);
297
- }
298
- $formatGuildMessage(normalized) {
299
- const senderInfo = {
300
- id: normalized.userId,
301
- name: normalized.nickname,
302
- };
303
- const result = Message.from(normalized.raw, {
304
- $id: normalized.messageId,
305
- $adapter: "icqq",
306
- $endpoint: this.$config.name,
307
- $sender: senderInfo,
308
- $channel: {
309
- id: normalized.channelId,
310
- type: "channel",
311
- parent: { type: "guild", id: normalized.guildId },
312
- },
313
- $content: normalized.content,
314
- $raw: normalized.rawMessage,
315
- $timestamp: normalized.timestampMs,
316
- $recall: async () => {
317
- await this.$recallMessage(result.$id);
318
- },
319
- $reply: async (content, quote) => {
320
- if (!Array.isArray(content))
321
- content = [content];
322
- if (quote) {
323
- content.unshift({
324
- type: "reply",
325
- data: { id: typeof quote === "boolean" ? result.$id : quote },
326
- });
327
- }
328
- return await this.adapter.sendMessage({
329
- context: "icqq",
330
- endpoint: this.$config.name,
331
- id: normalized.channelId,
332
- type: "channel",
333
- parent: { type: "guild", id: normalized.guildId },
334
- content,
335
- });
336
- },
337
- });
338
- return result;
339
- }
340
- handleNoticeEvent(event) {
341
- const dedupeKey = resolveSideEventDedupeKey(event, "notice");
342
- if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
343
- if (process.env.ICQQ_IPC_LOG_RAW === "1") {
344
- this.logger.debug(formatCompact({ ipc_dedupe: dedupeKey }));
345
- }
346
- return;
347
- }
348
- const notice = formatIcqqNotice(event, this.$config.name);
349
- this.adapter.emit("notice.receive", notice);
350
- this.logger.debug(formatCompact({
351
- notice: `${notice.$type}.${notice.$scene_type}.${notice.$sub_type}`,
352
- scene: `${notice.$scene_type}(${notice.$scene_id})`,
353
- endpoint: this.$id,
354
- }));
355
- }
356
- handleRequestEvent(event) {
357
- const dedupeKey = resolveSideEventDedupeKey(event, "request");
358
- if (!this.inboundDeduper.shouldProcess(dedupeKey)) {
359
- if (process.env.ICQQ_IPC_LOG_RAW === "1") {
360
- this.logger.debug(formatCompact({ ipc_dedupe: dedupeKey }));
361
- }
362
- return;
363
- }
364
- const request = formatIcqqRequest(event, this.$config.name, this.ipc);
365
- this.adapter.emit("request.receive", request);
366
- this.logger.debug(formatCompact({
367
- request: `${request.$type}.${request.$scene_type}.${request.$sub_type}`,
368
- scene: `${request.$scene_type}(${request.$scene_id})`,
369
- endpoint: this.$id,
370
- from: request.$actor.id,
371
- }));
372
- }
373
- handleMetaEvent(event) {
374
- const time = event.time ?? 0;
375
- const kind = event.meta_event_type ?? event.post_type ?? 'meta';
376
- const dedupeKey = `meta:${time}_${kind}_${event.sub_type ?? ''}`;
377
- if (!this.inboundDeduper.shouldProcess(dedupeKey))
378
- return;
379
- this.logger.debug(formatIcqqMetaLog(event));
380
- if (shouldRefreshListsOnMeta(event)) {
381
- void this.refreshLists().catch((e) => {
382
- this.logger.warn(formatCompact({
383
- op: "refresh_lists",
384
- ok: false,
385
- error: e instanceof Error ? e.message : String(e),
386
- }));
387
- });
388
- }
389
- }
390
- /** 调试 IPC 入站字段:默认 debug;设 ICQQ_IPC_LOG_RAW=1 则 info 打完整 payload */
391
- logIpcInboundPayload(data, normalized) {
392
- const ext = data;
393
- const keys = Object.keys(ext);
394
- const idHints = {};
395
- for (const key of keys) {
396
- if (/message|msg|seq|id|source/i.test(key)) {
397
- idHints[key] = ext[key];
398
- }
399
- }
400
- const preview = JSON.stringify(data);
401
- const compact = formatCompact({
402
- post_type: data.post_type ?? "legacy",
403
- message_type: data.message_type ?? data.type,
404
- ipc_message_id: normalized.messageId,
405
- id_source: normalized.idSource,
406
- ipc_keys: keys.join(","),
407
- ...(Object.keys(idHints).length ? { ipc_id_hints: JSON.stringify(idHints) } : {}),
408
- });
409
- this.logger.debug(compact);
410
- this.logger.debug(formatCompact({ ipc_payload: preview.slice(0, 1200) }));
411
- }
412
- $formatMessage(input) {
413
- const normalized = "messageId" in input
414
- ? input
415
- : normalizeIcqqInboundMessage(input);
416
- if (!normalized) {
417
- throw new Error("无法解析 icqq 入站消息");
418
- }
419
- const raw = normalized.raw;
420
- const senderInfo = {
421
- id: normalized.userId,
422
- name: normalized.nickname,
423
- };
424
- if (normalized.senderRole && normalized.senderRole !== "member") {
425
- senderInfo.role = normalized.senderRole;
426
- if (normalized.senderRole === "owner") {
427
- senderInfo.isOwner = true;
428
- senderInfo.permissions = ["owner"];
429
- }
430
- else if (normalized.senderRole === "admin") {
431
- senderInfo.isAdmin = true;
432
- senderInfo.permissions = ["admin"];
433
- }
434
- }
435
- const quoteId = Message.quoteIdFromContent(normalized.content) ??
436
- resolveIcqqQuoteIdFromEvent(normalized.raw);
437
- Message.alignReplySegments(normalized.content, quoteId);
438
- const result = Message.from(raw, {
439
- $id: normalized.messageId,
440
- $adapter: "icqq",
441
- $endpoint: this.$config.name,
442
- $sender: senderInfo,
443
- $channel: {
444
- id: normalized.channelId,
445
- type: normalized.channelType,
446
- ...(normalized.channelParentGroupId
447
- ? { parent: { type: 'group', id: normalized.channelParentGroupId } }
448
- : {}),
449
- },
450
- $content: normalized.content,
451
- $quote_id: quoteId,
452
- $raw: normalized.rawMessage,
453
- $timestamp: normalized.timestampMs,
454
- $recall: async () => {
455
- if (normalized.idSource === "synthetic") {
456
- this.logger.warn(formatCompact({
457
- op: "recall",
458
- endpoint: this.$id,
459
- ok: false,
460
- error: "no message_id in push",
461
- }));
462
- return;
463
- }
464
- await this.$recallMessage(result.$id);
465
- },
466
- $reply: async (content, quote) => {
467
- if (!Array.isArray(content))
468
- content = [content];
469
- if (quote) {
470
- content.unshift({
471
- type: "reply",
472
- data: { id: typeof quote === "boolean" ? result.$id : quote },
473
- });
474
- }
475
- return await this.adapter.sendMessage({
476
- ...result.$channel,
477
- context: "icqq",
478
- endpoint: this.$config.name,
479
- content,
480
- });
481
- },
482
- });
483
- return result;
484
- }
485
- /**
486
- * 有 source.message_id 时用 get_msg 拉全量正文;否则仅用 source 内联摘要。
487
- */
488
- async primeQuotedSourceCache(source) {
489
- if (!source)
490
- return;
491
- const quoteId = resolveQuoteIdFromIcqqSource(source);
492
- const s = source;
493
- const hasCanonicalId = quoteId &&
494
- s.message_id != null &&
495
- String(s.message_id).trim() === quoteId;
496
- if (hasCanonicalId) {
497
- try {
498
- await this.fetchQuotedMessagePayload(quoteId);
499
- return;
500
- }
501
- catch (e) {
502
- this.logger.debug(formatCompact({
503
- op: "quote_get_msg",
504
- message_id: quoteId,
505
- ok: false,
506
- error: e instanceof Error ? e.message : String(e),
507
- }));
508
- }
509
- }
510
- const sourcePayload = quotedPayloadFromIcqqSource(source);
511
- if (!sourcePayload)
512
- return;
513
- const enriched = await enrichQuotedPayloadWithForward(this.ipc, sourcePayload);
514
- this.quotedSourceCache.set(enriched.messageId, enriched);
515
- }
516
- async fetchQuotedMessagePayload(messageId) {
517
- const resp = await this.ipc.request(Actions.GET_MSG, {
518
- message_id: messageId,
519
- });
520
- if (!resp.ok) {
521
- throw new Error(resp.error ?? "get_msg failed");
522
- }
523
- const payload = parseIcqqGetMsgResponse(messageId, resp.data);
524
- const enriched = await enrichQuotedPayloadWithForward(this.ipc, payload, resp.data);
525
- if (isForwardPlaceholderPayload(enriched) &&
526
- !String(Array.isArray(enriched.content)
527
- ? segment.raw(enriched.content)
528
- : enriched.content ?? "").includes("[Merged chat history")) {
529
- this.logger.debug(formatCompact({
530
- op: "forward_unresolved",
531
- message_id: messageId,
532
- }));
533
- }
534
- this.quotedSourceCache.set(messageId, enriched);
535
- return enriched;
536
- }
537
- async $getMsg(messageId) {
538
- const cached = this.quotedSourceCache.get(messageId);
539
- if (cached) {
540
- return enrichQuotedPayloadWithForward(this.ipc, cached);
541
- }
542
- return this.fetchQuotedMessagePayload(messageId);
543
- }
544
- // ── 撤回 ───────────────────────────────────────────────────────────
545
- async $recallMessage(id) {
546
- const resp = await this.ipc.request(Actions.RECALL_MSG, {
547
- message_id: id,
548
- });
549
- if (!resp.ok) {
550
- this.logger.warn(formatCompact({
551
- op: "recall",
552
- endpoint: this.$id,
553
- ok: false,
554
- error: resp.error,
555
- }));
556
- }
557
- }
558
- // ── 发送消息 ───────────────────────────────────────────────────────
559
- async $sendMessage(options) {
560
- const outboundMedia = resolveIcqqOutboundMediaMode(this.$config);
561
- const expanded = expandInteractiveSegmentsInContent(options.content);
562
- const qrcodeContent = expanded;
563
- const content = materializeOutboundBase64(qrcodeContent, outboundMedia);
564
- const message = buildIcqqIpcMessageImpl(content);
565
- let action;
566
- let params;
567
- switch (options.type) {
568
- case "private": {
569
- const groupParent = options.parent?.type === "group" ? options.parent.id : undefined;
570
- if (groupParent) {
571
- action = Actions.SEND_TEMP_MSG;
572
- params = {
573
- group_id: Number(groupParent),
574
- user_id: Number(options.id),
575
- message,
576
- };
577
- }
578
- else {
579
- action = Actions.SEND_PRIVATE_MSG;
580
- params = { user_id: Number(options.id), message };
581
- }
582
- break;
583
- }
584
- case "group":
585
- action = Actions.SEND_GROUP_MSG;
586
- params = { group_id: Number(options.id), message };
587
- break;
588
- case "channel": {
589
- const guildId = options.parent?.type === "guild" ? options.parent.id : undefined;
590
- if (!guildId) {
591
- throw new Error('channel 发送需要 parent: { type: "guild", id: guild_id }');
592
- }
593
- action = Actions.GUILD_SEND_MSG;
594
- params = {
595
- guild_id: guildId,
596
- channel_id: options.id,
597
- message,
598
- };
599
- break;
600
- }
601
- default:
602
- throw new Error(`不支持的频道类型: ${options.type}`);
603
- }
604
- const resp = await this.ipc.request(action, params);
605
- if (!resp.ok) {
606
- this.logger.debug(formatCompact({
607
- op: action,
608
- ...params,
609
- }));
610
- throw new Error(`发送消息失败: ${resp.error}`);
611
- }
612
- const messageId = String(resp.data?.message_id ?? `sent_${Date.now()}`);
613
- this.logger.debug(`${this.$id} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
614
- return messageId;
615
- }
616
- // ── 消息回应 ───────────────────────────────────────────────────────
617
- /**
618
- * 表情符号到 reaction id 的映射
619
- * QQ 使用数字 ID 来标识表情,而不是 Unicode 字符
620
- */
621
- static EMOJI_MAP = {
622
- '⏳': '1468368274', // 沙漏
623
- '👍': '128077', // 竖起大拇指
624
- '❤️': '10084', // 红心
625
- '😊': '128522', // 微笑
626
- '🎉': '127881', // 派对
627
- '🔥': '128293', // 火
628
- '✅': '9989', // 勾选
629
- '❌': '10060', // 叉号
630
- '⭐': '11088', // 星星
631
- '💯': '128175', // 一百分
632
- };
633
- /**
634
- * 将表情符号转换为 reaction id
635
- */
636
- getEmojiId(emoji) {
637
- // 如果已经是数字 ID,直接返回
638
- if (/^\d+$/.test(emoji)) {
639
- return emoji;
640
- }
641
- // 从映射中查找
642
- const id = IcqqEndpoint.EMOJI_MAP[emoji];
643
- if (id) {
644
- return id;
645
- }
646
- // 默认返回 Unicode 码点
647
- return String(emoji.codePointAt(0) || 0);
648
- }
649
- /**
650
- * 添加消息回应(表情)
651
- *
652
- * @param messageId - 消息 ID
653
- * @param emoji - 表情符号或 reaction id
654
- * @returns 反应 ID,可用于后续移除
655
- */
656
- async $addReaction(messageId, emoji) {
657
- try {
658
- const emojiId = this.getEmojiId(emoji);
659
- const resp = await this.ipc.request(Actions.GROUP_SET_REACTION, {
660
- message_id: messageId,
661
- id: emojiId,
662
- });
663
- if (!resp.ok) {
664
- this.logger.warn(formatCompact({
665
- op: "add_reaction",
666
- endpoint: this.$id,
667
- message_id: messageId,
668
- emoji,
669
- id: emojiId,
670
- ok: false,
671
- error: resp.error,
672
- }));
673
- return null;
674
- }
675
- this.logger.debug(formatCompact({
676
- op: "add_reaction",
677
- endpoint: this.$id,
678
- message_id: messageId,
679
- emoji,
680
- id: emojiId,
681
- ok: true,
682
- }));
683
- // 返回 reaction id,供 $removeReaction 直接使用
684
- return emojiId;
685
- }
686
- catch (error) {
687
- this.logger.warn(formatCompact({
688
- op: "add_reaction",
689
- endpoint: this.$id,
690
- message_id: messageId,
691
- emoji,
692
- ok: false,
693
- error: error instanceof Error ? error.message : String(error),
694
- }));
695
- return null;
696
- }
697
- }
698
- /**
699
- * 移除消息回应
700
- *
701
- * @param messageId - 消息 ID
702
- * @param reactionId - 反应 ID(由 $addReaction 返回)
703
- */
704
- async $removeReaction(messageId, reactionId) {
705
- try {
706
- // 兼容两种格式:
707
- // 1) 旧格式 reaction_{id}_{timestamp}
708
- // 2) 新格式直接为 id
709
- const parts = reactionId.split('_');
710
- const emojiId = parts.length >= 2 ? parts[1] : reactionId;
711
- const resp = await this.ipc.request(Actions.GROUP_DEL_REACTION, {
712
- message_id: messageId,
713
- id: emojiId,
714
- });
715
- if (!resp.ok) {
716
- this.logger.warn(formatCompact({
717
- op: "remove_reaction",
718
- endpoint: this.$id,
719
- message_id: messageId,
720
- reaction_id: reactionId,
721
- id: emojiId,
722
- ok: false,
723
- error: resp.error,
724
- }));
725
- }
726
- else {
727
- this.logger.debug(formatCompact({
728
- op: "remove_reaction",
729
- endpoint: this.$id,
730
- message_id: messageId,
731
- reaction_id: reactionId,
732
- id: emojiId,
733
- ok: true,
734
- }));
735
- }
736
- }
737
- catch (error) {
738
- this.logger.warn(formatCompact({
739
- op: "remove_reaction",
740
- endpoint: this.$id,
741
- message_id: messageId,
742
- reaction_id: reactionId,
743
- ok: false,
744
- error: error instanceof Error ? error.message : String(error),
745
- }));
746
- }
747
- }
748
- }
749
- /** @deprecated 使用 `./cq-message.js` 导出 */
750
- (function (IcqqEndpoint) {
751
- IcqqEndpoint.parseCqMessage = parseCqMessageImpl;
752
- IcqqEndpoint.buildIcqqIpcMessage = buildIcqqIpcMessageImpl;
753
- IcqqEndpoint.toCqString = toCqStringImpl;
754
- })(IcqqEndpoint || (IcqqEndpoint = {}));
755
- //# sourceMappingURL=endpoint.js.map