evolclaw 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ import { logger } from '../utils/logger.js';
2
+ import { markdownToPlainText } from '../utils/format.js';
3
+ import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
4
+ // ── QQBotChannel ────────────────────────────────────────────────────────────
5
+ export class QQBotChannel {
6
+ config;
7
+ client = null;
8
+ connected = false;
9
+ messageHandler = null;
10
+ recallHandler;
11
+ seenMessages = new Map();
12
+ chatTypeCache = new Map();
13
+ msgIdCache = new Map();
14
+ groupOpenidCache = new Map();
15
+ markdownFailed = false;
16
+ cleanupInterval = null;
17
+ projectPathProvider = null;
18
+ constructor(config) {
19
+ this.config = config;
20
+ }
21
+ // ── Public helpers (testable) ──────────────────────────────────────────────
22
+ isDuplicate(msgId) {
23
+ if (this.seenMessages.has(msgId))
24
+ return true;
25
+ this.seenMessages.set(msgId, Date.now());
26
+ return false;
27
+ }
28
+ resolveChatId(event) {
29
+ return event.type === 'group' && event.groupOpenid ? event.groupOpenid : event.senderId;
30
+ }
31
+ shouldProcess(type) {
32
+ return type === 'c2c' || type === 'group';
33
+ }
34
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
35
+ async connect() {
36
+ const { appId, clientSecret } = this.config;
37
+ if (!appId || !clientSecret || appId.includes('your-') || clientSecret.includes('your-')) {
38
+ throw new Error('QQBot appId/clientSecret not configured');
39
+ }
40
+ const { QQBotClient } = await import('pure-qqbot');
41
+ this.client = new QQBotClient({
42
+ appId,
43
+ clientSecret,
44
+ typingKeepAlive: true,
45
+ logger: {
46
+ info: (msg) => logger.debug(`[QQBot/SDK] ${msg}`),
47
+ error: (msg) => logger.error(`[QQBot/SDK] ${msg}`),
48
+ debug: (msg) => logger.debug(`[QQBot/SDK] ${msg}`),
49
+ },
50
+ });
51
+ this.client.onMessage(async (event) => {
52
+ await this.handleIncoming(event);
53
+ });
54
+ await this.client.start();
55
+ this.client.startBackgroundRefresh();
56
+ this.connected = true;
57
+ // Hourly cleanup of old dedup entries
58
+ this.cleanupInterval = setInterval(() => {
59
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
60
+ for (const [id, ts] of this.seenMessages) {
61
+ if (ts < cutoff)
62
+ this.seenMessages.delete(id);
63
+ }
64
+ }, 60 * 60 * 1000);
65
+ logger.info('[QQBot] Connected via WebSocket Gateway v2');
66
+ }
67
+ async disconnect() {
68
+ this.connected = false;
69
+ if (this.cleanupInterval) {
70
+ clearInterval(this.cleanupInterval);
71
+ this.cleanupInterval = null;
72
+ }
73
+ if (this.client) {
74
+ try {
75
+ this.client.stopBackgroundRefresh();
76
+ this.client.stop();
77
+ }
78
+ catch { /* ignore */ }
79
+ this.client = null;
80
+ }
81
+ logger.info('[QQBot] Disconnected');
82
+ }
83
+ onMessage(handler) {
84
+ this.messageHandler = handler;
85
+ }
86
+ onRecall(handler) {
87
+ this.recallHandler = handler;
88
+ }
89
+ // ── Inbound message handling ───────────────────────────────────────────────
90
+ async handleIncoming(event) {
91
+ try {
92
+ // Filter: only c2c and group
93
+ if (!this.shouldProcess(event.type))
94
+ return;
95
+ // Dedup
96
+ if (event.messageId && this.isDuplicate(event.messageId)) {
97
+ logger.debug(`[QQBot] Duplicate message skipped: ${event.messageId}`);
98
+ return;
99
+ }
100
+ const chatId = this.resolveChatId(event);
101
+ const chatType = event.type === 'group' ? 'group' : 'private';
102
+ // Cache for outbound routing
103
+ this.chatTypeCache.set(chatId, chatType);
104
+ this.msgIdCache.set(chatId, event.messageId);
105
+ if (event.groupOpenid)
106
+ this.groupOpenidCache.set(chatId, event.groupOpenid);
107
+ if (!this.messageHandler)
108
+ return;
109
+ // Check for attachments (images/files)
110
+ const attachments = event.attachments || [];
111
+ const imageAttachments = attachments.filter(a => a.content_type?.startsWith('image/'));
112
+ const fileAttachments = attachments.filter(a => !a.content_type?.startsWith('image/'));
113
+ if (imageAttachments.length > 0) {
114
+ await this.handleImageAttachments(imageAttachments, event, chatId, chatType);
115
+ }
116
+ else if (fileAttachments.length > 0) {
117
+ await this.handleFileAttachments(fileAttachments, event, chatId, chatType);
118
+ }
119
+ else {
120
+ // Pure text
121
+ const text = (event.content || '').trim();
122
+ if (!text)
123
+ return;
124
+ await this.messageHandler({
125
+ channelId: chatId, content: text, chatType,
126
+ peerId: event.senderId || '', peerName: event.senderName,
127
+ messageId: event.messageId,
128
+ });
129
+ }
130
+ }
131
+ catch (error) {
132
+ logger.error('[QQBot] Failed to process incoming message:', error);
133
+ }
134
+ }
135
+ // ── Inbound media handling ─────────────────────────────────────────────────
136
+ async handleImageAttachments(attachments, event, chatId, chatType) {
137
+ const images = [];
138
+ for (const att of attachments) {
139
+ if (!att.url)
140
+ continue;
141
+ try {
142
+ const { safeFetch, validateImage } = await import('../utils/media-cache.js');
143
+ const buffer = await safeFetch(att.url, { skipSsrfCheck: true });
144
+ const result = await validateImage(buffer);
145
+ if (result.mime) {
146
+ images.push({ data: buffer.toString('base64'), mimeType: result.mime });
147
+ }
148
+ else {
149
+ logger.warn(`[QQBot] Image validation failed: ${'reason' in result ? result.reason : 'unknown'}`);
150
+ }
151
+ }
152
+ catch (error) {
153
+ logger.error('[QQBot] Failed to download image:', error);
154
+ }
155
+ }
156
+ const text = (event.content || '').trim();
157
+ const prompt = text || (images.length > 0 ? '用户发送了一张图片,请分析这张图片的内容。' : '[空消息]');
158
+ await this.messageHandler({
159
+ channelId: chatId, content: prompt, chatType,
160
+ peerId: event.senderId || '', peerName: event.senderName,
161
+ messageId: event.messageId,
162
+ images: images.length > 0 ? images : undefined,
163
+ });
164
+ }
165
+ async handleFileAttachments(attachments, event, chatId, chatType) {
166
+ for (const att of attachments) {
167
+ if (!att.url)
168
+ continue;
169
+ const fileName = att.filename || 'unknown';
170
+ try {
171
+ const { safeFetch, saveToUploads, sanitizeFileName } = await import('../utils/media-cache.js');
172
+ const projectPath = this.projectPathProvider
173
+ ? await this.projectPathProvider(chatId)
174
+ : process.cwd();
175
+ const buffer = await safeFetch(att.url, { skipSsrfCheck: true });
176
+ const { filePath } = saveToUploads(buffer, sanitizeFileName(fileName), projectPath);
177
+ await this.messageHandler({
178
+ channelId: chatId,
179
+ content: `用户发送了文件:${fileName}\n文件已保存到:${filePath}\n请使用 Read 工具读取并分析文件内容。`,
180
+ chatType, peerId: event.senderId || '', peerName: event.senderName,
181
+ messageId: event.messageId,
182
+ });
183
+ }
184
+ catch (error) {
185
+ logger.error('[QQBot] Failed to download file:', error);
186
+ await this.messageHandler({
187
+ channelId: chatId, content: `[文件下载失败] ${fileName}`,
188
+ chatType, peerId: event.senderId || '', peerName: event.senderName,
189
+ messageId: event.messageId,
190
+ });
191
+ }
192
+ }
193
+ }
194
+ // ── Outbound: text (markdown with fallback) ────────────────────────────────
195
+ async sendMessage(chatId, content) {
196
+ if (!this.client)
197
+ return;
198
+ const chatType = this.chatTypeCache.get(chatId);
199
+ const msgId = this.msgIdCache.get(chatId);
200
+ // Try Markdown first, fallback to plain text
201
+ if (!this.markdownFailed) {
202
+ try {
203
+ if (chatType === 'group') {
204
+ const groupOpenid = this.groupOpenidCache.get(chatId) || chatId;
205
+ await this.client.sendGroupMessage(groupOpenid, content, msgId);
206
+ }
207
+ else {
208
+ await this.client.sendPrivateMarkdown(chatId, content, msgId);
209
+ }
210
+ return; // success
211
+ }
212
+ catch (error) {
213
+ const errMsg = String(error?.message || error);
214
+ // Check if this is a markdown permission error
215
+ if (errMsg.includes('not support') || errMsg.includes('permission') ||
216
+ errMsg.includes('markdown') || error?.code === 304003 || error?.code === 304004) {
217
+ logger.warn('[QQBot] Markdown not supported, falling back to plain text globally');
218
+ this.markdownFailed = true;
219
+ // Fall through to plain text below
220
+ }
221
+ else {
222
+ // Other error — log and return, don't fallback
223
+ logger.error(`[QQBot] sendMessage failed for ${chatId}:`, errMsg);
224
+ return;
225
+ }
226
+ }
227
+ }
228
+ // Plain text fallback
229
+ try {
230
+ const plainText = markdownToPlainText(content);
231
+ if (chatType === 'group') {
232
+ const groupOpenid = this.groupOpenidCache.get(chatId) || chatId;
233
+ await this.client.sendGroupMessage(groupOpenid, plainText, msgId);
234
+ }
235
+ else {
236
+ await this.client.sendPrivateMessage(chatId, plainText, msgId);
237
+ }
238
+ }
239
+ catch (error) {
240
+ logger.error(`[QQBot] sendMessage (plaintext) failed for ${chatId}:`, error?.message || error);
241
+ }
242
+ }
243
+ // ── Outbound: image ────────────────────────────────────────────────────────
244
+ async sendImage(chatId, png) {
245
+ if (!this.client)
246
+ return;
247
+ try {
248
+ const fs = await import('fs');
249
+ const path = await import('path');
250
+ const os = await import('os');
251
+ const tmpPath = path.join(os.tmpdir(), `evolclaw-qqbot-${Date.now()}.png`);
252
+ fs.writeFileSync(tmpPath, png);
253
+ const chatType = this.chatTypeCache.get(chatId);
254
+ const msgId = this.msgIdCache.get(chatId);
255
+ if (chatType === 'group') {
256
+ const groupOpenid = this.groupOpenidCache.get(chatId) || chatId;
257
+ await this.client.sendGroupImage(groupOpenid, `file://${tmpPath}`, msgId);
258
+ }
259
+ else {
260
+ await this.client.sendPrivateImage(chatId, `file://${tmpPath}`, msgId);
261
+ }
262
+ try {
263
+ fs.unlinkSync(tmpPath);
264
+ }
265
+ catch { /* ignore */ }
266
+ }
267
+ catch (error) {
268
+ logger.error(`[QQBot] sendImage failed for ${chatId}:`, error?.message || error);
269
+ }
270
+ }
271
+ // ── Outbound: file ─────────────────────────────────────────────────────────
272
+ async sendFile(chatId, filePath) {
273
+ if (!this.client)
274
+ return;
275
+ try {
276
+ const fs = await import('fs');
277
+ const header = Buffer.alloc(12);
278
+ const fd = fs.openSync(filePath, 'r');
279
+ fs.readSync(fd, header, 0, 12, 0);
280
+ fs.closeSync(fd);
281
+ const { fileTypeFromBuffer } = await import('file-type');
282
+ const ftype = await fileTypeFromBuffer(header);
283
+ if (ftype && ftype.mime.startsWith('image/')) {
284
+ const buf = fs.readFileSync(filePath);
285
+ return this.sendImage(chatId, buf);
286
+ }
287
+ const chatType = this.chatTypeCache.get(chatId);
288
+ const msgId = this.msgIdCache.get(chatId);
289
+ if (chatType === 'group') {
290
+ const groupOpenid = this.groupOpenidCache.get(chatId) || chatId;
291
+ await this.client.sendGroupFile(groupOpenid, `file://${filePath}`, msgId);
292
+ }
293
+ else {
294
+ await this.client.sendPrivateFile(chatId, `file://${filePath}`, msgId);
295
+ }
296
+ }
297
+ catch (error) {
298
+ logger.error(`[QQBot] sendFile failed for ${chatId}:`, error?.message || error);
299
+ }
300
+ }
301
+ }
302
+ // ── Plugin ─────────────────────────────────────────────────────────────────────
303
+ function isValidCredential(value) {
304
+ return !!value && !value.includes('your-') && !value.includes('placeholder');
305
+ }
306
+ export class QQBotChannelPlugin {
307
+ name = 'qqbot';
308
+ isEnabled(config) {
309
+ const raw = config.channels?.qqbot;
310
+ if (!raw)
311
+ return false;
312
+ if (Array.isArray(raw)) {
313
+ return raw.some(inst => inst.enabled !== false && isValidCredential(inst.appId) && isValidCredential(inst.clientSecret));
314
+ }
315
+ if (raw.enabled === false)
316
+ return false;
317
+ return isValidCredential(raw.appId) && isValidCredential(raw.clientSecret);
318
+ }
319
+ async createChannels(config) {
320
+ const instances = normalizeChannelInstances(config.channels?.qqbot, 'qqbot');
321
+ const result = [];
322
+ for (const inst of instances) {
323
+ if (inst.enabled === false)
324
+ continue;
325
+ if (!isValidCredential(inst.appId) || !isValidCredential(inst.clientSecret))
326
+ continue;
327
+ const channel = new QQBotChannel({
328
+ appId: inst.appId,
329
+ clientSecret: inst.clientSecret,
330
+ });
331
+ const adapter = {
332
+ channelName: inst.name,
333
+ sendText: (id, text) => channel.sendMessage(id, text),
334
+ sendFile: (id, filePath) => channel.sendFile(id, filePath),
335
+ sendImage: (id, png) => channel.sendImage(id, png),
336
+ };
337
+ const policy = {
338
+ canSwitchProject: (_chatType, identity) => identity === 'owner' || identity === 'admin',
339
+ canListProjects: (_chatType, identity) => identity === 'owner' || identity === 'admin',
340
+ canCreateSession: () => true,
341
+ canDeleteSession: () => true,
342
+ canImportCliSession: (_chatType, identity) => identity === 'owner' || identity === 'admin',
343
+ messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
344
+ showMiddleResult: (chatType, identity) => {
345
+ const mode = getChannelShowActivities(config, inst.name);
346
+ if (mode === 'none')
347
+ return false;
348
+ if (mode === 'dm-only')
349
+ return chatType === 'private';
350
+ if (mode === 'owner-dm-only')
351
+ return chatType === 'private' && identity === 'owner';
352
+ return true;
353
+ },
354
+ showIdleMonitor: (chatType, identity) => {
355
+ const mode = getChannelShowActivities(config, inst.name);
356
+ if (mode === 'none')
357
+ return false;
358
+ if (mode === 'dm-only')
359
+ return chatType === 'private';
360
+ if (mode === 'owner-dm-only')
361
+ return chatType === 'private' && identity === 'owner';
362
+ return true;
363
+ },
364
+ accumulateErrors: () => true,
365
+ };
366
+ const options = {
367
+ fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
368
+ supportsImages: true,
369
+ flushDelay: inst.flushDelay,
370
+ };
371
+ result.push({
372
+ channelType: 'qqbot',
373
+ adapter,
374
+ channel,
375
+ policy,
376
+ options,
377
+ connect: () => channel.connect(),
378
+ disconnect: () => channel.disconnect(),
379
+ onProjectPathRequest: () => Promise.resolve(config.projects?.defaultPath || process.cwd()),
380
+ });
381
+ }
382
+ return result;
383
+ }
384
+ async createChannel(config) {
385
+ const instances = await this.createChannels(config);
386
+ if (instances.length === 0) {
387
+ throw new Error('QQBot config missing or invalid');
388
+ }
389
+ return instances[0];
390
+ }
391
+ }
@@ -4,7 +4,11 @@ import path from 'path';
4
4
  import { resolvePaths } from '../paths.js';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { sanitizeFileName, saveToUploads, safeFetch } from '../utils/media-cache.js';
7
+ import { markdownToPlainText } from '../utils/format.js';
7
8
  const CHANNEL_VERSION = '1.0.0';
9
+ const ILINK_APP_ID = 'bot';
10
+ // iLink-App-ClientVersion: major<<16 | minor<<8 | patch (uint32)
11
+ const ILINK_APP_CLIENT_VERSION = String((1 << 16) | (0 << 8) | 0); // 1.0.0 = 65536
8
12
  const DEFAULT_LONG_POLL_TIMEOUT_MS = 35_000;
9
13
  const DEFAULT_API_TIMEOUT_MS = 15_000;
10
14
  const DEFAULT_CONFIG_TIMEOUT_MS = 10_000;
@@ -67,38 +71,6 @@ async function downloadMedia(cdnMedia, hexKey) {
67
71
  return encrypted; // 无 key = 明文
68
72
  return decryptAesEcb(encrypted, parseAesKey(aesKeyBase64));
69
73
  }
70
- // ── Markdown → Plain Text ───────────────────────────────────────────────────
71
- function markdownToPlainText(text) {
72
- let result = text;
73
- // Code blocks: strip fences, keep content
74
- result = result.replace(/```[^\n]*\n?([\s\S]*?)```/g, (_, code) => code.trim());
75
- // Images: remove entirely
76
- result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
77
- // Links: keep display text only
78
- result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
79
- // Tables: remove separator rows
80
- result = result.replace(/^\|[\s:|-]+\|$/gm, '');
81
- result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split('|').map(cell => cell.trim()).join(' '));
82
- // Bold/italic
83
- result = result.replace(/\*\*(.+?)\*\*/g, '$1');
84
- result = result.replace(/\*(.+?)\*/g, '$1');
85
- result = result.replace(/__(.+?)__/g, '$1');
86
- result = result.replace(/_(.+?)_/g, '$1');
87
- // Strikethrough
88
- result = result.replace(/~~(.+?)~~/g, '$1');
89
- // Inline code
90
- result = result.replace(/`([^`]+)`/g, '$1');
91
- // Headers
92
- result = result.replace(/^#{1,6}\s+/gm, '');
93
- // Blockquotes
94
- result = result.replace(/^>\s?/gm, '');
95
- // Horizontal rules
96
- result = result.replace(/^[-*_]{3,}$/gm, '');
97
- // List markers
98
- result = result.replace(/^(\s*)[-*+]\s/gm, '$1');
99
- result = result.replace(/^(\s*)\d+\.\s/gm, '$1');
100
- return result.trim();
101
- }
102
74
  // ── Message Text Extraction ─────────────────────────────────────────────────
103
75
  function extractTextFromMessage(msg) {
104
76
  if (!msg.item_list?.length)
@@ -129,6 +101,7 @@ function extractTextFromMessage(msg) {
129
101
  export class WechatChannel {
130
102
  config;
131
103
  messageHandler;
104
+ recallHandler;
132
105
  abortController;
133
106
  connected = false;
134
107
  // 内部状态(不外泄到核心层)
@@ -153,6 +126,9 @@ export class WechatChannel {
153
126
  onMessage(handler) {
154
127
  this.messageHandler = handler;
155
128
  }
129
+ onRecall(handler) {
130
+ this.recallHandler = handler;
131
+ }
156
132
  /** 注册 session 过期通知回调(用于跨渠道通知用户) */
157
133
  onSessionExpiredNotify(handler) {
158
134
  this.onSessionExpired = handler;
@@ -179,6 +155,13 @@ export class WechatChannel {
179
155
  catch {
180
156
  // ignore
181
157
  }
158
+ // 通知 ilink 后端:bot 上线
159
+ try {
160
+ await this.notifyLifecycle('notifystart');
161
+ }
162
+ catch (err) {
163
+ logger.warn('[WeChat] notifyStart failed during startup (ignored):', err);
164
+ }
182
165
  this.abortController = new AbortController();
183
166
  // 启动长轮询(不 await,后台运行)
184
167
  this.pollLoop(this.abortController.signal).catch(err => {
@@ -196,6 +179,13 @@ export class WechatChannel {
196
179
  this.abortController.abort();
197
180
  this.abortController = undefined;
198
181
  }
182
+ // 通知 ilink 后端:bot 下线
183
+ try {
184
+ await this.notifyLifecycle('notifystop');
185
+ }
186
+ catch (err) {
187
+ logger.warn('[WeChat] notifyStop failed during shutdown (ignored):', err);
188
+ }
199
189
  logger.info('[WeChat] Channel disconnected');
200
190
  }
201
191
  /** Get current connection status */
@@ -638,6 +628,12 @@ export class WechatChannel {
638
628
  logger.info(`[WeChat] Sent media to ${to}, type=${item.type}`);
639
629
  }
640
630
  // ── ilink API Helpers ─────────────────────────────────────────────────
631
+ /** Notify ilink backend of bot lifecycle events (start/stop). */
632
+ async notifyLifecycle(action) {
633
+ const body = JSON.stringify({ base_info: { channel_version: CHANNEL_VERSION } });
634
+ await this.apiFetch(`ilink/bot/msg/${action}`, body, DEFAULT_CONFIG_TIMEOUT_MS);
635
+ logger.info(`[WeChat] ${action} succeeded`);
636
+ }
641
637
  async apiFetch(endpoint, body, timeoutMs, externalSignal) {
642
638
  const base = this.config.baseUrl.endsWith('/') ? this.config.baseUrl : `${this.config.baseUrl}/`;
643
639
  const url = new URL(endpoint, base).toString();
@@ -676,6 +672,8 @@ export class WechatChannel {
676
672
  'AuthorizationType': 'ilink_bot_token',
677
673
  'Content-Length': String(Buffer.byteLength(body, 'utf-8')),
678
674
  'X-WECHAT-UIN': wechatUin,
675
+ 'iLink-App-Id': ILINK_APP_ID,
676
+ 'iLink-App-ClientVersion': ILINK_APP_CLIENT_VERSION,
679
677
  };
680
678
  if (this.config.token?.trim()) {
681
679
  headers['Authorization'] = `Bearer ${this.config.token.trim()}`;
@@ -707,7 +705,7 @@ export class WechatChannel {
707
705
  });
708
706
  }
709
707
  }
710
- import { normalizeChannelInstances } from '../config.js';
708
+ import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
711
709
  export class WechatChannelPlugin {
712
710
  name = 'wechat';
713
711
  isEnabled(config) {
@@ -735,14 +733,14 @@ export class WechatChannelPlugin {
735
733
  sendFile: (id, filePath) => channel.sendFile(id, filePath),
736
734
  };
737
735
  const policy = {
738
- canSwitchProject: (chatType, identity) => identity === 'owner',
739
- canListProjects: (chatType, identity) => identity === 'owner',
736
+ canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
737
+ canListProjects: (chatType, identity) => identity === 'owner' || identity === 'admin',
740
738
  canCreateSession: (chatType, identity) => true,
741
739
  canDeleteSession: (chatType, identity) => true,
742
- canImportCliSession: (chatType, identity) => identity === 'owner',
740
+ canImportCliSession: (chatType, identity) => identity === 'owner' || identity === 'admin',
743
741
  messagePrefix: (chatType, peerName) => '',
744
742
  showMiddleResult: (chatType, identity) => {
745
- const mode = inst.showActivities ?? config.showActivities ?? 'all';
743
+ const mode = getChannelShowActivities(config, inst.name);
746
744
  if (mode === 'none')
747
745
  return false;
748
746
  if (mode === 'dm-only')
@@ -752,7 +750,7 @@ export class WechatChannelPlugin {
752
750
  return true;
753
751
  },
754
752
  showIdleMonitor: (chatType, identity) => {
755
- const mode = inst.showActivities ?? config.showActivities ?? 'all';
753
+ const mode = getChannelShowActivities(config, inst.name);
756
754
  if (mode === 'none')
757
755
  return false;
758
756
  if (mode === 'dm-only')