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