@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.
- package/CHANGELOG.md +59 -0
- package/README.md +32 -437
- package/adapters/icqq.ts +30 -0
- package/agent/tools/announce.ts +21 -0
- package/agent/tools/essence.ts +22 -0
- package/agent/tools/friend_list.ts +19 -0
- package/agent/tools/group_files.ts +27 -0
- package/agent/tools/group_invite.ts +20 -0
- package/agent/tools/group_list.ts +20 -0
- package/agent/tools/group_sign.ts +19 -0
- package/agent/tools/list_muted.ts +22 -0
- package/agent/tools/poke.ts +20 -0
- package/agent/tools/send_user_like.ts +22 -0
- package/agent/tools/set_anonymous.ts +22 -0
- package/agent/tools/set_title.ts +25 -0
- package/lib/agent-prompt.d.ts +0 -1
- package/lib/agent-prompt.js +0 -1
- package/lib/cq-message.d.ts +0 -1
- package/lib/cq-message.js +0 -1
- package/lib/endpoint.d.ts +45 -97
- package/lib/endpoint.js +357 -628
- package/lib/forward-msg.d.ts +1 -2
- package/lib/forward-msg.js +4 -2
- package/lib/icqq-agent-deps.d.ts +25 -0
- package/lib/icqq-agent-deps.js +32 -0
- package/lib/icqq-guild.d.ts +1 -2
- package/lib/icqq-guild.js +4 -2
- package/lib/icqq-inbound.d.ts +10 -1
- package/lib/icqq-inbound.js +27 -3
- package/lib/index.d.ts +6 -18
- package/lib/index.js +6 -50
- package/lib/ipc-client.d.ts +0 -1
- package/lib/ipc-client.js +0 -1
- package/lib/outbound-media.d.ts +0 -1
- package/lib/outbound-media.js +0 -1
- package/lib/protocol.d.ts +73 -2
- package/lib/protocol.js +124 -2
- package/lib/types.d.ts +0 -1
- package/lib/types.js +0 -1
- package/package.json +51 -43
- package/plugin.ts +32 -0
- package/schema.json +35 -0
- package/src/agent-prompt.ts +1 -2
- package/src/endpoint.ts +399 -800
- package/src/forward-msg.ts +2 -2
- package/src/icqq-agent-deps.ts +57 -0
- package/src/icqq-guild.ts +1 -2
- package/src/icqq-inbound.ts +31 -2
- package/src/index.ts +35 -69
- package/src/protocol.ts +191 -1
- package/client/ICQQManagement.tsx +0 -147
- package/client/LoginAssistPanel.tsx +0 -300
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -34
- package/lib/adapter.d.ts +0 -46
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -128
- package/lib/adapter.js.map +0 -1
- package/lib/agent-prompt.d.ts.map +0 -1
- package/lib/agent-prompt.js.map +0 -1
- package/lib/cq-message.d.ts.map +0 -1
- package/lib/cq-message.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/forward-msg.d.ts.map +0 -1
- package/lib/forward-msg.js.map +0 -1
- package/lib/get-msg.d.ts +0 -3
- package/lib/get-msg.d.ts.map +0 -1
- package/lib/get-msg.js +0 -46
- package/lib/get-msg.js.map +0 -1
- package/lib/icqq-guild.d.ts.map +0 -1
- package/lib/icqq-guild.js.map +0 -1
- package/lib/icqq-inbound.d.ts.map +0 -1
- package/lib/icqq-inbound.js.map +0 -1
- package/lib/icqq-login-assist-bridge.d.ts +0 -5
- package/lib/icqq-login-assist-bridge.d.ts.map +0 -1
- package/lib/icqq-login-assist-bridge.js +0 -148
- package/lib/icqq-login-assist-bridge.js.map +0 -1
- package/lib/icqq-side-events.d.ts +0 -34
- package/lib/icqq-side-events.d.ts.map +0 -1
- package/lib/icqq-side-events.js +0 -194
- package/lib/icqq-side-events.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/ipc-client.d.ts.map +0 -1
- package/lib/ipc-client.js.map +0 -1
- package/lib/login-assist-routes.d.ts +0 -7
- package/lib/login-assist-routes.d.ts.map +0 -1
- package/lib/login-assist-routes.js +0 -29
- package/lib/login-assist-routes.js.map +0 -1
- package/lib/login-ipc-contract.d.ts +0 -44
- package/lib/login-ipc-contract.d.ts.map +0 -1
- package/lib/login-ipc-contract.js +0 -35
- package/lib/login-ipc-contract.js.map +0 -1
- package/lib/outbound-media.d.ts.map +0 -1
- package/lib/outbound-media.js.map +0 -1
- package/lib/protocol.d.ts.map +0 -1
- package/lib/protocol.js.map +0 -1
- package/lib/routes.d.ts +0 -8
- package/lib/routes.d.ts.map +0 -1
- package/lib/routes.js +0 -67
- package/lib/routes.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/tools/index.d.ts +0 -10
- package/lib/tools/index.d.ts.map +0 -1
- package/lib/tools/index.js +0 -341
- package/lib/tools/index.js.map +0 -1
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -151
- package/src/get-msg.ts +0 -56
- package/src/icqq-login-assist-bridge.ts +0 -196
- package/src/icqq-side-events.ts +0 -228
- package/src/login-assist-routes.ts +0 -39
- package/src/login-ipc-contract.ts +0 -52
- package/src/routes.ts +0 -83
- package/src/segment-mapper.ts +0 -1
- package/src/tools/index.ts +0 -412
- /package/{skills/icqq → agent}/PERMITS.md +0 -0
package/lib/endpoint.js
CHANGED
|
@@ -1,668 +1,244 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
subscriptions = [];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
67
|
-
const uin = Number(this.$config.name);
|
|
68
|
-
const rpc = this.$config.rpc;
|
|
62
|
+
async #bindIpcSession() {
|
|
69
63
|
// 旧连接上的订阅句柄已失效,不再对死 socket 发 unsubscribe
|
|
70
|
-
this
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
76
|
+
#scheduleReconnect() {
|
|
77
|
+
if (this.#options.config.autoReconnect === false)
|
|
98
78
|
return;
|
|
99
|
-
if (this
|
|
79
|
+
if (this.#intentionalDisconnect)
|
|
100
80
|
return;
|
|
101
|
-
if (this
|
|
81
|
+
if (this.#reconnectRunning)
|
|
102
82
|
return;
|
|
103
|
-
this
|
|
104
|
-
void this
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
102
|
+
if (this.#intentionalDisconnect)
|
|
122
103
|
break;
|
|
123
104
|
try {
|
|
124
|
-
await this
|
|
125
|
-
|
|
126
|
-
op:
|
|
127
|
-
endpoint: this
|
|
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 (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
return this.guildCatalog.getGuildChannelList();
|
|
132
|
+
open() {
|
|
133
|
+
this.#open = true;
|
|
177
134
|
}
|
|
178
|
-
|
|
179
|
-
|
|
135
|
+
close() {
|
|
136
|
+
this.#open = false;
|
|
180
137
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this
|
|
138
|
+
async stop() {
|
|
139
|
+
this.#intentionalDisconnect = true;
|
|
140
|
+
this.#open = false;
|
|
184
141
|
this.ipc?.setOnRemoteDisconnect(null);
|
|
185
|
-
for (const sub of this.
|
|
142
|
+
for (const sub of this.#subscriptions.splice(0)) {
|
|
186
143
|
await sub.unsubscribe().catch(() => { });
|
|
187
144
|
}
|
|
188
|
-
this
|
|
189
|
-
this
|
|
190
|
-
|
|
191
|
-
this.
|
|
192
|
-
this
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
const content =
|
|
563
|
-
|
|
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 (
|
|
567
|
-
case
|
|
568
|
-
|
|
569
|
-
|
|
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:
|
|
180
|
+
params = { group_id: parsed.groupId, message };
|
|
586
181
|
break;
|
|
587
|
-
case
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
664
|
-
op:
|
|
665
|
-
endpoint: this
|
|
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
|
-
|
|
675
|
-
op:
|
|
676
|
-
endpoint: this
|
|
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
|
-
|
|
687
|
-
op:
|
|
688
|
-
endpoint: this
|
|
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.
|
|
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
|
-
|
|
716
|
-
op:
|
|
717
|
-
endpoint: this
|
|
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
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
731
|
-
|
|
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
|
-
|
|
738
|
-
op:
|
|
739
|
-
endpoint: this
|
|
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
|